Account Activity enhancements (#1196)

* use the "follow" button as an "unblock" button on the profiles of blocked users

* use the "follow" button as an "unblock" button on the profiles of blocked users

* add an icon to the profiles that can be clicked to mute/unmute the user

* add an icon to the profiles that can be clicked to mute/unmute the user

* Fix view issues

* Fix view issues

* Implement swipe to refresh for Account layout

* Implement swipe to refresh handler at the account screen

* Implement swipe to refresh

* Correct account refresh

* Show Progress Bar

* Show Progress Bar

* Move "itSelf" check into the viewModel

* Change methods access level

* Change TimelineFragment newInstance overload

* Change avatarSize type to Float

* Replace ImageButton with MaterialButton

* Update account activity swipe to refresh colors

* Refactor code

* Refactor code

* Fix crash on moved account refresh

* Show moved account stats

* Update mute button behaviour

* Show tabs and content for moved accounts

* Fix crash on tablet
This commit is contained in:
pandasoft0 2019-05-15 13:43:16 +03:00 committed by Konrad Pozniak
parent 2cd25b6ce0
commit ae5d8b8633
13 changed files with 890 additions and 580 deletions

View file

@ -64,7 +64,7 @@ import javax.inject.Inject
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector, LinkListener {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<androidx.fragment.app.Fragment>
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var viewModelFactory: ViewModelFactory
@ -72,12 +72,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private val accountFieldAdapter = AccountFieldAdapter(this)
private lateinit var accountId: String
private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false
private var muting: Boolean = false
private var showingReblogs: Boolean = false
private var isSelf: Boolean = false
private var loadedAccount: Account? = null
// fields for scroll animation
@ -95,7 +93,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private var textColorPrimary: Int = 0
@ColorInt
private var textColorSecondary: Int = 0
@Px
private var avatarSize: Float = 0f
@Px
private var titleVisibleHeight: Int = 0
@ -106,46 +104,118 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
REQUESTED
}
private var adapter: AccountPagerAdapter? = null
private lateinit var adapter: AccountPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeNotificationBarTransparent()
setContentView(R.layout.activity_account)
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
viewModel.accountData.observe(this, Observer<Resource<Account>> {
when (it) {
is Success -> onAccountChanged(it.data)
is Error -> {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { reload() }
.show()
}
}
})
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
// Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID))
if (viewModel.isSelf) {
updateButtons()
}
if (it is Error) {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { reload() }
.show()
hideFab = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fabHide", false)
loadResources()
setupToolbar()
setupTabs()
setupAccountViews()
setupRefreshLayout()
subscribeObservables()
}
/**
* Load colors and dimensions from resources
*/
private fun loadResources() {
toolbarColor = ThemeUtils.getColor(this, R.attr.toolbar_background_color)
backgroundColor = ThemeUtils.getColor(this, android.R.attr.colorBackground)
statusBarColorTransparent = ContextCompat.getColor(this, R.color.header_background_filter)
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
textColorPrimary = ThemeUtils.getColor(this, android.R.attr.textColorPrimary)
textColorSecondary = ThemeUtils.getColor(this, android.R.attr.textColorSecondary)
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.
accountFloatingActionButton.hide()
accountFollowButton.hide()
accountMuteButton.hide()
accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields
accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this)
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)
}
accountFollowers.setOnClickListener(accountListClickListener)
accountFollowing.setOnClickListener(accountListClickListener)
accountStatuses.setOnClickListener {
// Make nice ripple effect on tab
accountTabLayout.getTabAt(0)!!.select()
val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0)
poorTabView.isPressed = true
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
}
}
/**
* Init timeline tabs
*/
private fun setupTabs() {
// Setup the tabs and timeline pager.
adapter = AccountPagerAdapter(supportFragmentManager, viewModel.accountId)
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))
adapter.setPageTitles(pageTitles)
accountFragmentViewPager.pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
val pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark)
accountFragmentViewPager.setPageMarginDrawable(pageMarginDrawable)
accountFragmentViewPager.adapter = adapter
accountFragmentViewPager.offscreenPageLimit = 2
accountTabLayout.setupWithViewPager(accountFragmentViewPager)
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?) {}
})
}
val decorView = window.decorView
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
window.statusBarColor = Color.TRANSPARENT
setContentView(R.layout.activity_account)
val intent = intent
accountId = intent.getStringExtra(KEY_ACCOUNT_ID)
/**
* Setup toolbar
*/
private fun setupToolbar() {
// set toolbar top margin according to system window insets
accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
val top = insets.systemWindowInsetTop
@ -162,17 +232,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
hideFab = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fabHide", false)
toolbarColor = ThemeUtils.getColor(this, R.attr.toolbar_background_color)
backgroundColor = ThemeUtils.getColor(this, android.R.attr.colorBackground)
statusBarColorTransparent = ContextCompat.getColor(this, R.color.header_background_filter)
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
textColorPrimary = ThemeUtils.getColor(this, android.R.attr.textColorPrimary)
textColorSecondary = ThemeUtils.getColor(this, android.R.attr.textColorSecondary)
avatarSize = resources.getDimensionPixelSize(R.dimen.account_activity_avatar_size).toFloat()
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
ThemeUtils.setDrawableTint(this, accountToolbar.navigationIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
ThemeUtils.setDrawableTint(this, accountToolbar.overflowIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
@ -201,7 +260,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
ThemeUtils.setDrawableTint(context, accountToolbar.overflowIcon, attribute)
}
if (hideFab && !isSelf && !blocking) {
if (hideFab && !viewModel.isSelf && !blocking) {
if (verticalOffset > oldOffset) {
accountFloatingActionButton.show()
}
@ -228,99 +287,98 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
accountToolbar.setBackgroundColor(evaluatedToolbarColor)
accountHeaderInfoContainer.setBackgroundColor(evaluatedTabBarColor)
accountTabLayout.setBackgroundColor(evaluatedTabBarColor)
swipeToRefreshLayout.isEnabled = verticalOffset == 0
}
})
// Initialise the default UI states.
accountFloatingActionButton.hide()
accountFollowButton.hide()
accountFollowsYouTextView.hide()
// Obtain information to fill out the profile.
viewModel.obtainAccount(accountId)
val activeAccount = accountManager.activeAccount
if (accountId == activeAccount?.accountId) {
isSelf = true
updateButtons()
} else {
isSelf = false
viewModel.obtainRelationship(accountId)
}
// setup the RecyclerView for the account fields
accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this)
accountFieldList.adapter = accountFieldAdapter
// Setup the tabs and timeline pager.
adapter = AccountPagerAdapter(supportFragmentManager, accountId)
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))
adapter?.setPageTitles(pageTitles)
accountFragmentViewPager.pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
val pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark)
accountFragmentViewPager.setPageMarginDrawable(pageMarginDrawable)
accountFragmentViewPager.adapter = adapter
accountFragmentViewPager.offscreenPageLimit = 2
accountTabLayout.setupWithViewPager(accountFragmentViewPager)
accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let {
(adapter?.getFragment(tab.position) as? ReselectableFragment)?.onReselect()
}
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
window.statusBarColor = Color.TRANSPARENT
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
/**
* Subscribe to data loaded at the view model
*/
private fun subscribeObservables() {
viewModel.accountData.observe(this, Observer<Resource<Account>> {
when (it) {
is Success -> onAccountChanged(it.data)
is Error -> {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
}
})
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
}
override fun onTabSelected(tab: TabLayout.Tab?) {}
if (it is Error) {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
})
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, accountId)
startActivityWithSlideInAnimation(accountListIntent)
}
accountFollowers.setOnClickListener(accountListClickListener)
accountFollowing.setOnClickListener(accountListClickListener)
accountStatuses.setOnClickListener {
// Make nice ripple effect on tab
accountTabLayout.getTabAt(0)!!.select()
val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0)
poorTabView.isPressed = true
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
/**
* Setup swipe to refresh layout
*/
private fun setupRefreshLayout() {
swipeToRefreshLayout.setOnRefreshListener {
viewModel.refresh()
adapter.refreshContent()
}
viewModel.isRefreshing.observe(this, Observer { isRefreshing ->
swipeToRefreshLayout.isRefreshing = isRefreshing == true
})
swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeToRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(this,
android.R.attr.colorBackground))
}
private fun onAccountChanged(account: Account?) {
if (account != null) {
loadedAccount = account
loadedAccount = account ?: return
val usernameFormatted = getString(R.string.status_username_format, account.username)
accountUsernameTextView.text = usernameFormatted
accountDisplayNameTextView.text = CustomEmojiHelper.emojifyString(account.name, account.emojis, accountDisplayNameTextView)
if (supportActionBar != null) {
try {
supportActionBar?.title = EmojiCompat.get().process(account.name)
} catch (e: IllegalStateException) {
supportActionBar?.title = account.name
}
val subtitle = String.format(getString(R.string.status_username_format),
account.username)
supportActionBar?.subtitle = subtitle
}
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged()
accountLockedImageView.visible(account.locked)
accountBadgeTextView.visible(account.bot)
updateAccountAvatar()
updateToolbar()
updateMovedAccount()
updateRemoteAccount()
updateAccountStats()
accountMuteButton.setOnClickListener {
viewModel.changeMuteState()
updateMuteButton()
}
}
/**
* Load account's avatar and header image
*/
private fun updateAccountAvatar() {
loadedAccount?.let { account ->
Glide.with(this)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
@ -330,6 +388,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
.centerCrop()
.into(accountHeaderImageView)
accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar)
@ -338,18 +397,33 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
startActivity(intent, options.toBundle())
}
}
}
accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged()
/**
* Update toolbar views for loaded account
*/
private fun updateToolbar() {
loadedAccount?.let { account ->
try {
supportActionBar?.title = EmojiCompat.get().process(account.name)
} catch (e: IllegalStateException) {
supportActionBar?.title = account.name
}
supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username)
}
}
if (account.moved != null) {
val movedAccount = account.moved
/**
* Update moved account info
*/
private fun updateMovedAccount() {
loadedAccount?.moved?.let { movedAccount ->
accountMovedView.show()
accountMovedView?.show()
// necessary because accountMovedView is now replaced in layout hierachy
findViewById<View>(R.id.accountMovedView).setOnClickListener {
findViewById<View>(R.id.accountMovedViewLayout).setOnClickListener {
onViewAccount(movedAccount.id)
}
@ -369,21 +443,29 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
movedIcon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
accountFollowers.hide()
accountFollowing.hide()
accountStatuses.hide()
accountTabLayout.hide()
accountFragmentViewPager.hide()
}
}
/**
* Check is account remote and update info if so
*/
private fun updateRemoteAccount() {
loadedAccount?.let { account ->
if (account.isRemote()) {
accountRemoveView.show()
accountRemoveView.setOnClickListener {
LinkHelper.openLink(account.url, this)
}
}
}
}
/**
* Update account stat info
*/
private fun updateAccountStats() {
loadedAccount?.let { account ->
val numberFormat = NumberFormat.getNumberInstance()
accountFollowersTextView.text = numberFormat.format(account.followersCount)
accountFollowingTextView.text = numberFormat.format(account.followingCount)
@ -392,19 +474,25 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
accountFloatingActionButton.setOnClickListener { mention() }
accountFollowButton.setOnClickListener {
if (isSelf) {
if (viewModel.isSelf) {
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
startActivity(intent)
return@setOnClickListener
}
when (followState) {
AccountActivity.FollowState.NOT_FOLLOWING -> {
viewModel.changeFollowState(accountId)
if (blocking) {
viewModel.changeBlockState()
return@setOnClickListener
}
AccountActivity.FollowState.REQUESTED -> {
when (followState) {
FollowState.NOT_FOLLOWING -> {
viewModel.changeFollowState()
}
FollowState.REQUESTED -> {
showFollowRequestPendingDialog()
}
AccountActivity.FollowState.FOLLOWING -> {
FollowState.FOLLOWING -> {
showUnfollowWarningDialog()
}
}
@ -413,11 +501,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(KEY_ACCOUNT_ID, accountId)
super.onSaveInstanceState(outState)
}
private fun onRelationshipChanged(relation: Relationship) {
followState = when {
relation.following -> FollowState.FOLLOWING
@ -433,53 +516,67 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
updateButtons()
}
private fun reload() {
viewModel.obtainAccount(accountId, true)
viewModel.obtainRelationship(accountId)
}
private fun updateFollowButton() {
if (isSelf) {
if (viewModel.isSelf) {
accountFollowButton.setText(R.string.action_edit_own_profile)
return
}
if (blocking) {
accountFollowButton.setText(R.string.action_unblock)
return
}
when (followState) {
AccountActivity.FollowState.NOT_FOLLOWING -> {
FollowState.NOT_FOLLOWING -> {
accountFollowButton.setText(R.string.action_follow)
}
AccountActivity.FollowState.REQUESTED -> {
FollowState.REQUESTED -> {
accountFollowButton.setText(R.string.state_follow_requested)
}
AccountActivity.FollowState.FOLLOWING -> {
FollowState.FOLLOWING -> {
accountFollowButton.setText(R.string.action_unfollow)
}
}
}
private fun updateMuteButton() {
if (muting) {
accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp)
} else {
accountMuteButton.hide()
}
}
private fun updateButtons() {
invalidateOptionsMenu()
if (!blocking && loadedAccount?.moved == null) {
if (loadedAccount?.moved == null) {
accountFollowButton.show()
updateFollowButton()
if (isSelf) {
if (blocking || viewModel.isSelf) {
accountFloatingActionButton.hide()
accountMuteButton.hide()
} else {
accountFloatingActionButton.show()
if (muting)
accountMuteButton.show()
else
accountMuteButton.hide()
updateMuteButton()
}
} else {
accountFloatingActionButton.hide()
accountFollowButton.hide()
accountMuteButton.hide()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.account_toolbar, menu)
if (!isSelf) {
if (!viewModel.isSelf) {
val follow = menu.findItem(R.id.action_follow)
follow.title = if (followState == FollowState.NOT_FOLLOWING) {
getString(R.string.action_follow)
@ -529,7 +626,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private fun showFollowRequestPendingDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.dialog_message_cancel_follow_request)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) }
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
@ -537,7 +634,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private fun showUnfollowWarningDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.dialog_unfollow_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) }
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
@ -585,20 +682,20 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
return true
}
R.id.action_follow -> {
viewModel.changeFollowState(accountId)
viewModel.changeFollowState()
return true
}
R.id.action_block -> {
viewModel.changeBlockState(accountId)
viewModel.changeBlockState()
return true
}
R.id.action_mute -> {
viewModel.changeMuteState(accountId)
viewModel.changeMuteState()
return true
}
R.id.action_show_reblogs -> {
viewModel.changeShowReblogsState(accountId)
viewModel.changeShowReblogsState()
return true
}
}
@ -606,7 +703,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
}
override fun getActionButton(): FloatingActionButton? {
return if (!isSelf && !blocking) {
return if (!viewModel.isSelf && !blocking) {
accountFloatingActionButton
} else null
}

View file

@ -16,11 +16,13 @@
package com.keylesspalace.tusky;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment;

View file

@ -30,6 +30,7 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasSu
}
}
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

View file

@ -16,11 +16,13 @@
package com.keylesspalace.tusky;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.TimelineFragment;

View file

@ -77,7 +77,7 @@ class NetworkModule {
.apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
}
}
.build()

View file

@ -33,6 +33,7 @@ import com.keylesspalace.tusky.ViewMediaActivity
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.ThemeUtils
import com.keylesspalace.tusky.util.hide
@ -53,22 +54,26 @@ import javax.inject.Inject
* Fragment with multiple columns of media previews for the specified account.
*/
class AccountMediaFragment : BaseFragment(), Injectable {
class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
companion object {
@JvmStatic
fun newInstance(accountId: String): 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)
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"
}
private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
@Inject
lateinit var api: MastodonApi
@ -78,6 +83,8 @@ class AccountMediaFragment : BaseFragment(), Injectable {
private var fetchingStatus = FetchingStatus.NOT_FETCHING
private var isVisibleToUser: Boolean = false
private var accountId: String?=null
private val callback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
fetchingStatus = FetchingStatus.NOT_FETCHING
@ -85,6 +92,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) {
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
topProgressBar?.hide()
statusView.show()
if (t is IOException) {
statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
@ -105,6 +113,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) {
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
topProgressBar?.hide()
val body = response.body()
body?.let { fetched ->
@ -115,6 +124,8 @@ class AccountMediaFragment : BaseFragment(), Injectable {
result.addAll(AttachmentViewData.list(status))
}
adapter.addTop(result)
if (result.isNotEmpty())
recyclerView.scrollToPosition(0)
if (statuses.isEmpty()) {
statusView.show()
@ -152,6 +163,11 @@ class AccountMediaFragment : BaseFragment(), Injectable {
}
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 onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false)
@ -171,24 +187,15 @@ class AccountMediaFragment : BaseFragment(), Injectable {
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
if (isSwipeToRefreshEnabled) {
swipeRefreshLayout.setOnRefreshListener {
statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener
currentCall = 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)
}
currentCall?.enqueue(callback)
refresh()
}
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
}
statusView.visibility = View.GONE
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@ -212,6 +219,22 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isVisibleToUser) doInitialLoadingIfNeeded()
}
private fun refresh() {
statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
currentCall = 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)
}
currentCall?.enqueue(callback)
if (!isSwipeToRefreshEnabled)
topProgressBar?.show()
}
// That's sort of an optimization to only load media once user has opened the tab
// Attention: can be called before *any* lifecycle method!
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
@ -224,12 +247,14 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) {
statusView.hide()
}
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null)
currentCall?.enqueue(callback)
}
else if (needToRefresh)
refresh()
needToRefresh = false
}
private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) {
@ -321,4 +346,13 @@ class AccountMediaFragment : BaseFragment(), Injectable {
}
}
}
override fun refreshContent() {
if (isAdded)
refresh()
else
needToRefresh = true
}
}

View file

@ -48,6 +48,7 @@ import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.RefreshableFragment;
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
@ -82,6 +83,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
@ -92,6 +94,7 @@ import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -108,12 +111,15 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
Injectable, ReselectableFragment {
Injectable, ReselectableFragment, RefreshableFragment {
private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id";
private static final String ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh";
private static final int LOAD_AT_ONCE = 30;
private boolean isSwipeToRefreshEnabled = true;
private boolean isNeedRefresh;
public enum Kind {
HOME,
@ -146,6 +152,7 @@ public class TimelineFragment extends SFragment implements
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private ProgressBar progressBar;
private ContentLoadingProgressBar topProgressBar;
private BackgroundMessageView statusView;
private TimelineAdapter adapter;
@ -182,18 +189,19 @@ public class TimelineFragment extends SFragment implements
});
public static TimelineFragment newInstance(Kind kind) {
TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle();
arguments.putString(KIND_ARG, kind.name());
fragment.setArguments(arguments);
return fragment;
return newInstance(kind, null);
}
public static TimelineFragment newInstance(Kind kind, String hashtagOrId) {
public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId) {
return newInstance(kind, hashtagOrId, true);
}
public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh) {
TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle();
arguments.putString(KIND_ARG, kind.name());
arguments.putString(HASHTAG_OR_ID_ARG, hashtagOrId);
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh);
fragment.setArguments(arguments);
return fragment;
}
@ -213,6 +221,8 @@ public class TimelineFragment extends SFragment implements
adapter = new TimelineAdapter(dataSource, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
}
@Override
@ -224,6 +234,7 @@ public class TimelineFragment extends SFragment implements
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
progressBar = rootView.findViewById(R.id.progressBar);
statusView = rootView.findViewById(R.id.statusView);
topProgressBar = rootView.findViewById(R.id.topProgressBar);
setupSwipeRefreshLayout();
setupRecyclerView();
@ -236,6 +247,8 @@ public class TimelineFragment extends SFragment implements
this.sendInitialRequest();
} else {
progressBar.setVisibility(View.GONE);
if (isNeedRefresh)
onRefresh();
}
return rootView;
@ -388,12 +401,15 @@ public class TimelineFragment extends SFragment implements
}
private void setupSwipeRefreshLayout() {
swipeRefreshLayout.setEnabled(isSwipeToRefreshEnabled);
if (isSwipeToRefreshEnabled) {
Context context = swipeRefreshLayout.getContext();
swipeRefreshLayout.setOnRefreshListener(this);
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context,
android.R.attr.colorBackground));
}
}
private void setupRecyclerView() {
recyclerView.setAccessibilityDelegateCompat(
@ -524,8 +540,10 @@ public class TimelineFragment extends SFragment implements
@Override
public void onRefresh() {
if (isSwipeToRefreshEnabled)
swipeRefreshLayout.setEnabled(true);
this.statusView.setVisibility(View.GONE);
isNeedRefresh = false;
if (this.initialUpdateFailed) {
updateCurrent();
} else {
@ -936,6 +954,9 @@ public class TimelineFragment extends SFragment implements
private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId,
@Nullable String sinceIdMinusOne,
final FetchEnd fetchEnd, final int pos) {
if (isAdded() && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && progressBar.getVisibility() != View.VISIBLE) && !isSwipeToRefreshEnabled)
topProgressBar.show();
if (kind == Kind.HOME) {
TimelineRequestMode mode;
// allow getting old statuses/fallbacks for network only for for bottom loading
@ -1015,6 +1036,8 @@ public class TimelineFragment extends SFragment implements
break;
}
}
if (isAdded()) {
topProgressBar.hide();
updateBottomLoadingState(fetchEnd);
progressBar.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false);
@ -1025,10 +1048,12 @@ public class TimelineFragment extends SFragment implements
this.statusView.setVisibility(View.GONE);
}
}
}
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
if (isAdded()) {
swipeRefreshLayout.setRefreshing(false);
topProgressBar.hide();
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
Placeholder placeholder = statuses.get(position).asLeftOrNull();
@ -1267,7 +1292,10 @@ public class TimelineFragment extends SFragment implements
adapter.notifyItemRangeInserted(position, count);
Context context = getContext();
if (position == 0 && context != null) {
if (isSwipeToRefreshEnabled)
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
else
recyclerView.scrollToPosition(0);
}
}
}
@ -1362,4 +1390,12 @@ public class TimelineFragment extends SFragment implements
public void onReselect() {
jumpToTop();
}
@Override
public void refreshContent() {
if (isAdded())
onRefresh();
else
isNeedRefresh = true;
}
}

View file

@ -0,0 +1,11 @@
package com.keylesspalace.tusky.interfaces
/**
* Created by pandasoft (joelpyska1@gmail.com) on 04/04/2019.
*/
interface RefreshableFragment {
/**
* Call this method to refresh fragment content
*/
fun refreshContent()
}

View file

@ -20,6 +20,10 @@ import android.view.ViewGroup;
import com.keylesspalace.tusky.fragment.AccountMediaFragment;
import com.keylesspalace.tusky.fragment.TimelineFragment;
import com.keylesspalace.tusky.interfaces.RefreshableFragment;
import java.util.HashSet;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -34,6 +38,8 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
private SparseArray<Fragment> fragments = new SparseArray<>(TAB_COUNT);
private final Set<Integer> pagesToRefresh = new HashSet<>();
public AccountPagerAdapter(FragmentManager manager, String accountId) {
super(manager);
this.accountId = accountId;
@ -48,16 +54,16 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
public Fragment getItem(int position) {
switch (position) {
case 0: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId);
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId,false);
}
case 1: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId);
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId,false);
}
case 2: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId);
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId,false);
}
case 3: {
return AccountMediaFragment.newInstance(accountId);
return AccountMediaFragment.newInstance(accountId,false);
}
default: {
throw new AssertionError("Page " + position + " is out of AccountPagerAdapter bounds");
@ -76,6 +82,11 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
Object fragment = super.instantiateItem(container, position);
if (fragment instanceof Fragment)
fragments.put(position, (Fragment) fragment);
if (pagesToRefresh.contains(position)) {
if (fragment instanceof RefreshableFragment)
((RefreshableFragment) fragment).refreshContent();
pagesToRefresh.remove(position);
}
return fragment;
}
@ -94,4 +105,16 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
public Fragment getFragment(int position) {
return fragments.get(position);
}
public void refreshContent(){
for (int i=0;i<getCount();i++){
Fragment fragment = getFragment(i);
if (fragment instanceof RefreshableFragment){
((RefreshableFragment) fragment).refreshContent();
}
else{
pagesToRefresh.add(i);
}
}
}
}

View file

@ -3,6 +3,7 @@ package com.keylesspalace.tusky.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.network.MastodonApi
@ -18,8 +19,9 @@ import javax.inject.Inject
class AccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
): ViewModel() {
private val eventHub: EventHub,
private val accountManager: AccountManager
) : ViewModel() {
val accountData = MutableLiveData<Resource<Account>>()
val relationshipData = MutableLiveData<Resource<Relationship>>()
@ -32,10 +34,15 @@ class AccountViewModel @Inject constructor(
}
}
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
private var isDataLoading = false
fun obtainAccount(accountId: String, reload: Boolean = false) {
if(accountData.value == null || reload) {
lateinit var accountId: String
var isSelf = false
private fun obtainAccount(reload: Boolean = false) {
if (accountData.value == null || reload) {
isDataLoading = true
accountData.postValue(Loading())
val call = mastodonApi.account(accountId)
@ -47,10 +54,14 @@ class AccountViewModel @Inject constructor(
} else {
accountData.postValue(Error())
}
isDataLoading = false
isRefreshing.postValue(false)
}
override fun onFailure(call: Call<Account>, t: Throwable) {
accountData.postValue(Error())
isDataLoading = false
isRefreshing.postValue(false)
}
})
@ -58,8 +69,8 @@ class AccountViewModel @Inject constructor(
}
}
fun obtainRelationship(accountId: String, reload: Boolean = false) {
if(relationshipData.value == null || reload) {
private fun obtainRelationship(reload: Boolean = false) {
if (relationshipData.value == null || reload) {
relationshipData.postValue(Loading())
@ -86,47 +97,47 @@ class AccountViewModel @Inject constructor(
}
}
fun changeFollowState(id: String) {
fun changeFollowState() {
val relationship = relationshipData.value?.data
if (relationship?.following == true || relationship?.requested == true) {
changeRelationship(RelationShipAction.UNFOLLOW, id)
changeRelationship(RelationShipAction.UNFOLLOW)
} else {
changeRelationship(RelationShipAction.FOLLOW, id)
changeRelationship(RelationShipAction.FOLLOW)
}
}
fun changeBlockState(id: String) {
fun changeBlockState() {
if (relationshipData.value?.data?.blocking == true) {
changeRelationship(RelationShipAction.UNBLOCK, id)
changeRelationship(RelationShipAction.UNBLOCK)
} else {
changeRelationship(RelationShipAction.BLOCK, id)
changeRelationship(RelationShipAction.BLOCK)
}
}
fun changeMuteState(id: String) {
fun changeMuteState() {
if (relationshipData.value?.data?.muting == true) {
changeRelationship(RelationShipAction.UNMUTE, id)
changeRelationship(RelationShipAction.UNMUTE)
} else {
changeRelationship(RelationShipAction.MUTE, id)
changeRelationship(RelationShipAction.MUTE)
}
}
fun changeShowReblogsState(id: String) {
fun changeShowReblogsState() {
if (relationshipData.value?.data?.showingReblogs == true) {
changeRelationship(RelationShipAction.FOLLOW, id, false)
changeRelationship(RelationShipAction.FOLLOW, false)
} else {
changeRelationship(RelationShipAction.FOLLOW, id, true)
changeRelationship(RelationShipAction.FOLLOW, true)
}
}
private fun changeRelationship(relationshipAction: RelationShipAction, id: String, showReblogs: Boolean = true) {
private fun changeRelationship(relationshipAction: RelationShipAction, showReblogs: Boolean = true) {
val relation = relationshipData.value?.data
val account = accountData.value?.data
if(relation != null && account != null) {
if (relation != null && account != null) {
// optimistically post new state for faster response
val newRelation = when(relationshipAction) {
val newRelation = when (relationshipAction) {
RelationShipAction.FOLLOW -> {
if (account.locked) {
relation.copy(requested = true)
@ -151,10 +162,11 @@ class AccountViewModel @Inject constructor(
relationshipData.postValue(Success(relationship))
when (relationshipAction) {
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(id))
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(id))
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(id))
else -> {}
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId))
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId))
else -> {
}
}
} else {
@ -168,13 +180,13 @@ class AccountViewModel @Inject constructor(
}
}
val call = when(relationshipAction) {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(id, showReblogs)
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(id)
RelationShipAction.BLOCK -> mastodonApi.blockAccount(id)
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(id)
RelationShipAction.MUTE -> mastodonApi.muteAccount(id)
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(id)
val call = when (relationshipAction) {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs)
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId)
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
}
call.enqueue(callback)
@ -189,6 +201,27 @@ class AccountViewModel @Inject constructor(
disposable.dispose()
}
fun refresh() {
reload(true)
}
private fun reload(isReload: Boolean = false) {
if (isDataLoading)
return
accountId.let {
obtainAccount(isReload)
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
}

View file

@ -45,5 +45,16 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
app:layout_constraintTop_toTopOf="parent"
android:indeterminate="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeToRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/accountCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textDirection="anyRtl"
android:fillViewport="true">
android:fillViewport="true"
android:textDirection="anyRtl">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/accountAppBarLayout"
@ -43,17 +48,50 @@
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideAvatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/account_activity_avatar_size" />
<Button
android:id="@+id/accountFollowButton"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/accountMuteButton"
app:layout_constraintTop_toTopOf="parent"
tools:text="Follow" />
tools:text="Follow Requested" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountMuteButton"
style="@style/TuskyButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="6dp"
android:minWidth="0dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scaleType="centerInside"
app:icon="@drawable/ic_unmute_24dp"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton"
app:layout_constraintEnd_toStartOf="@id/accountFollowButton"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guideAvatar"
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/accountDisplayNameTextView"
@ -135,6 +173,7 @@
android:id="@+id/accountNoteTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary"
@ -150,14 +189,6 @@
tools:itemCount="2"
tools:listitem="@layout/item_account_field" />
<ViewStub
android:id="@+id/accountMovedView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/accountMovedView"
android:layout="@layout/view_account_moved"
app:layout_constraintTop_toBottomOf="@id/accountFieldList" />
<TextView
android:id="@+id/accountRemoveView"
android:layout_width="match_parent"
@ -167,9 +198,24 @@
android:lineSpacingMultiplier="1.1"
android:text="@string/label_remote_account"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/accountMovedView"
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
tools:visibility="visible" />
<ViewStub
android:id="@+id/accountMovedView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/accountMovedViewLayout"
android:layout="@layout/view_account_moved"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrierRemote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="accountMovedView,accountMovedViewLayout" />
<LinearLayout
android:id="@+id/accountStatuses"
android:layout_width="wrap_content"
@ -179,13 +225,14 @@
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
app:layout_constraintTop_toBottomOf="@id/barrierRemote">
<TextView
android:id="@+id/accountStatusesTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
@ -209,7 +256,7 @@
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowers"
app:layout_constraintStart_toEndOf="@id/accountStatuses"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
app:layout_constraintTop_toBottomOf="@id/barrierRemote">
<TextView
android:id="@+id/accountFollowingTextView"
@ -239,7 +286,7 @@
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/accountFollowing"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
app:layout_constraintTop_toBottomOf="@id/barrierRemote">
<TextView
android:id="@+id/accountFollowersTextView"
@ -260,6 +307,8 @@
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- top margin equal to statusbar size will be set programmatically -->
@ -278,6 +327,7 @@
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
app:tabGravity="center"
app:tabMode="scrollable"
@ -313,4 +363,5 @@
app:layout_anchorGravity="top"
app:layout_scrollFlags="scroll"
app:srcCompat="@drawable/avatar_default" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View file

@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
@ -38,5 +37,15 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error"
tools:visibility="visible"/>
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
app:layout_constraintTop_toTopOf="parent"
android:indeterminate="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>