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 { class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector, LinkListener {
@Inject @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<androidx.fragment.app.Fragment> lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -72,12 +72,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private val accountFieldAdapter = AccountFieldAdapter(this) private val accountFieldAdapter = AccountFieldAdapter(this)
private lateinit var accountId: String
private var followState: FollowState = FollowState.NOT_FOLLOWING private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false private var blocking: Boolean = false
private var muting: Boolean = false private var muting: Boolean = false
private var showingReblogs: Boolean = false private var showingReblogs: Boolean = false
private var isSelf: Boolean = false
private var loadedAccount: Account? = null private var loadedAccount: Account? = null
// fields for scroll animation // fields for scroll animation
@ -95,7 +93,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private var textColorPrimary: Int = 0 private var textColorPrimary: Int = 0
@ColorInt @ColorInt
private var textColorSecondary: Int = 0 private var textColorSecondary: Int = 0
@Px
private var avatarSize: Float = 0f private var avatarSize: Float = 0f
@Px @Px
private var titleVisibleHeight: Int = 0 private var titleVisibleHeight: Int = 0
@ -106,46 +104,118 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
REQUESTED REQUESTED
} }
private var adapter: AccountPagerAdapter? = null private lateinit var adapter: AccountPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
makeNotificationBarTransparent()
setContentView(R.layout.activity_account)
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java] viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
viewModel.accountData.observe(this, Observer<Resource<Account>> { // Obtain information to fill out the profile.
when (it) { viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID))
is Success -> onAccountChanged(it.data)
is Error -> { if (viewModel.isSelf) {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) updateButtons()
.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()
} }
} }
})
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
}
if (it is Error) { override fun onTabUnselected(tab: TabLayout.Tab?) {}
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { reload() } override fun onTabSelected(tab: TabLayout.Tab?) {}
.show()
}
}) })
}
val decorView = window.decorView /**
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN * Setup toolbar
window.statusBarColor = Color.TRANSPARENT */
private fun setupToolbar() {
setContentView(R.layout.activity_account)
val intent = intent
accountId = intent.getStringExtra(KEY_ACCOUNT_ID)
// set toolbar top margin according to system window insets // set toolbar top margin according to system window insets
accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
val top = insets.systemWindowInsetTop val top = insets.systemWindowInsetTop
@ -162,17 +232,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(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.navigationIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
ThemeUtils.setDrawableTint(this, accountToolbar.overflowIcon, 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) ThemeUtils.setDrawableTint(context, accountToolbar.overflowIcon, attribute)
} }
if (hideFab && !isSelf && !blocking) { if (hideFab && !viewModel.isSelf && !blocking) {
if (verticalOffset > oldOffset) { if (verticalOffset > oldOffset) {
accountFloatingActionButton.show() accountFloatingActionButton.show()
} }
@ -228,99 +287,98 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
accountToolbar.setBackgroundColor(evaluatedToolbarColor) accountToolbar.setBackgroundColor(evaluatedToolbarColor)
accountHeaderInfoContainer.setBackgroundColor(evaluatedTabBarColor) accountHeaderInfoContainer.setBackgroundColor(evaluatedTabBarColor)
accountTabLayout.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. private fun makeNotificationBarTransparent() {
viewModel.obtainAccount(accountId) 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
}
val activeAccount = accountManager.activeAccount /**
* Subscribe to data loaded at the view model
if (accountId == activeAccount?.accountId) { */
isSelf = true private fun subscribeObservables() {
updateButtons() viewModel.accountData.observe(this, Observer<Resource<Account>> {
} else { when (it) {
isSelf = false is Success -> onAccountChanged(it.data)
viewModel.obtainRelationship(accountId) is Error -> {
} Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { viewModel.refresh() }
// setup the RecyclerView for the account fields .show()
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()
} }
} }
})
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {} if (it is Error) {
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
override fun onTabSelected(tab: TabLayout.Tab?) {} .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 * Setup swipe to refresh layout
accountTabLayout.getTabAt(0)!!.select() */
val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) private fun setupRefreshLayout() {
poorTabView.isPressed = true swipeToRefreshLayout.setOnRefreshListener {
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) 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?) { private fun onAccountChanged(account: Account?) {
if (account != null) { loadedAccount = account ?: return
loadedAccount = account
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), val usernameFormatted = getString(R.string.status_username_format, account.username)
account.username) accountUsernameTextView.text = usernameFormatted
supportActionBar?.subtitle = subtitle accountDisplayNameTextView.text = CustomEmojiHelper.emojifyString(account.name, account.emojis, accountDisplayNameTextView)
}
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
accountLockedImageView.visible(account.locked) val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
accountBadgeTextView.visible(account.bot) 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) Glide.with(this)
.load(account.avatar) .load(account.avatar)
.placeholder(R.drawable.avatar_default) .placeholder(R.drawable.avatar_default)
@ -330,6 +388,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
.centerCrop() .centerCrop()
.into(accountHeaderImageView) .into(accountHeaderImageView)
accountAvatarImageView.setOnClickListener { avatarView -> accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar) val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar)
@ -338,52 +397,75 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
} }
}
}
accountFieldAdapter.fields = account.fields ?: emptyList() /**
accountFieldAdapter.emojis = account.emojis ?: emptyList() * Update toolbar views for loaded account
accountFieldAdapter.notifyDataSetChanged() */
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 // necessary because accountMovedView is now replaced in layout hierachy
findViewById<View>(R.id.accountMovedView).setOnClickListener { findViewById<View>(R.id.accountMovedViewLayout).setOnClickListener {
onViewAccount(movedAccount.id) onViewAccount(movedAccount.id)
}
accountMovedDisplayName.text = movedAccount.name
accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
Glide.with(this)
.load(movedAccount.avatar)
.placeholder(R.drawable.avatar_default)
.into(accountMovedAvatar)
accountMovedText.text = getString(R.string.account_moved_description, movedAccount.displayName)
// 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?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
accountFollowers.hide()
accountFollowing.hide()
accountStatuses.hide()
accountTabLayout.hide()
accountFragmentViewPager.hide()
} }
accountMovedDisplayName.text = movedAccount.name
accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
Glide.with(this)
.load(movedAccount.avatar)
.placeholder(R.drawable.avatar_default)
.into(accountMovedAvatar)
accountMovedText.text = getString(R.string.account_moved_description, movedAccount.displayName)
// 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?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
}
}
/**
* Check is account remote and update info if so
*/
private fun updateRemoteAccount() {
loadedAccount?.let { account ->
if (account.isRemote()) { if (account.isRemote()) {
accountRemoveView.show() accountRemoveView.show()
accountRemoveView.setOnClickListener { accountRemoveView.setOnClickListener {
LinkHelper.openLink(account.url, this) LinkHelper.openLink(account.url, this)
} }
} }
}
}
/**
* Update account stat info
*/
private fun updateAccountStats() {
loadedAccount?.let { account ->
val numberFormat = NumberFormat.getNumberInstance() val numberFormat = NumberFormat.getNumberInstance()
accountFollowersTextView.text = numberFormat.format(account.followersCount) accountFollowersTextView.text = numberFormat.format(account.followersCount)
accountFollowingTextView.text = numberFormat.format(account.followingCount) accountFollowingTextView.text = numberFormat.format(account.followingCount)
@ -392,19 +474,25 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
accountFloatingActionButton.setOnClickListener { mention() } accountFloatingActionButton.setOnClickListener { mention() }
accountFollowButton.setOnClickListener { accountFollowButton.setOnClickListener {
if (isSelf) { if (viewModel.isSelf) {
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
startActivity(intent) startActivity(intent)
return@setOnClickListener return@setOnClickListener
} }
if (blocking) {
viewModel.changeBlockState()
return@setOnClickListener
}
when (followState) { when (followState) {
AccountActivity.FollowState.NOT_FOLLOWING -> { FollowState.NOT_FOLLOWING -> {
viewModel.changeFollowState(accountId) viewModel.changeFollowState()
} }
AccountActivity.FollowState.REQUESTED -> { FollowState.REQUESTED -> {
showFollowRequestPendingDialog() showFollowRequestPendingDialog()
} }
AccountActivity.FollowState.FOLLOWING -> { FollowState.FOLLOWING -> {
showUnfollowWarningDialog() 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) { private fun onRelationshipChanged(relation: Relationship) {
followState = when { followState = when {
relation.following -> FollowState.FOLLOWING relation.following -> FollowState.FOLLOWING
@ -433,53 +516,67 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
updateButtons() updateButtons()
} }
private fun reload() {
viewModel.obtainAccount(accountId, true)
viewModel.obtainRelationship(accountId)
}
private fun updateFollowButton() { private fun updateFollowButton() {
if (isSelf) { if (viewModel.isSelf) {
accountFollowButton.setText(R.string.action_edit_own_profile) accountFollowButton.setText(R.string.action_edit_own_profile)
return return
} }
if (blocking) {
accountFollowButton.setText(R.string.action_unblock)
return
}
when (followState) { when (followState) {
AccountActivity.FollowState.NOT_FOLLOWING -> { FollowState.NOT_FOLLOWING -> {
accountFollowButton.setText(R.string.action_follow) accountFollowButton.setText(R.string.action_follow)
} }
AccountActivity.FollowState.REQUESTED -> { FollowState.REQUESTED -> {
accountFollowButton.setText(R.string.state_follow_requested) accountFollowButton.setText(R.string.state_follow_requested)
} }
AccountActivity.FollowState.FOLLOWING -> { FollowState.FOLLOWING -> {
accountFollowButton.setText(R.string.action_unfollow) accountFollowButton.setText(R.string.action_unfollow)
} }
} }
} }
private fun updateMuteButton() {
if (muting) {
accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp)
} else {
accountMuteButton.hide()
}
}
private fun updateButtons() { private fun updateButtons() {
invalidateOptionsMenu() invalidateOptionsMenu()
if (!blocking && loadedAccount?.moved == null) { if (loadedAccount?.moved == null) {
accountFollowButton.show() accountFollowButton.show()
updateFollowButton() updateFollowButton()
if (isSelf) { if (blocking || viewModel.isSelf) {
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountMuteButton.hide()
} else { } else {
accountFloatingActionButton.show() accountFloatingActionButton.show()
if (muting)
accountMuteButton.show()
else
accountMuteButton.hide()
updateMuteButton()
} }
} else { } else {
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountFollowButton.hide() accountFollowButton.hide()
accountMuteButton.hide()
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.account_toolbar, menu) menuInflater.inflate(R.menu.account_toolbar, menu)
if (!isSelf) { if (!viewModel.isSelf) {
val follow = menu.findItem(R.id.action_follow) val follow = menu.findItem(R.id.action_follow)
follow.title = if (followState == FollowState.NOT_FOLLOWING) { follow.title = if (followState == FollowState.NOT_FOLLOWING) {
getString(R.string.action_follow) getString(R.string.action_follow)
@ -529,7 +626,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private fun showFollowRequestPendingDialog() { private fun showFollowRequestPendingDialog() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.dialog_message_cancel_follow_request) .setMessage(R.string.dialog_message_cancel_follow_request)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) } .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
@ -537,7 +634,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private fun showUnfollowWarningDialog() { private fun showUnfollowWarningDialog() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.dialog_unfollow_warning) .setMessage(R.string.dialog_unfollow_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) } .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
@ -585,20 +682,20 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
return true return true
} }
R.id.action_follow -> { R.id.action_follow -> {
viewModel.changeFollowState(accountId) viewModel.changeFollowState()
return true return true
} }
R.id.action_block -> { R.id.action_block -> {
viewModel.changeBlockState(accountId) viewModel.changeBlockState()
return true return true
} }
R.id.action_mute -> { R.id.action_mute -> {
viewModel.changeMuteState(accountId) viewModel.changeMuteState()
return true return true
} }
R.id.action_show_reblogs -> { R.id.action_show_reblogs -> {
viewModel.changeShowReblogsState(accountId) viewModel.changeShowReblogsState()
return true return true
} }
} }
@ -606,7 +703,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
} }
override fun getActionButton(): FloatingActionButton? { override fun getActionButton(): FloatingActionButton? {
return if (!isSelf && !blocking) { return if (!viewModel.isSelf && !blocking) {
accountFloatingActionButton accountFloatingActionButton
} else null } else null
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide 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. * Fragment with multiple columns of media previews for the specified account.
*/ */
class AccountMediaFragment : BaseFragment(), Injectable { class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
companion object { companion object {
@JvmStatic @JvmStatic
fun newInstance(accountId: String): AccountMediaFragment { fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
val fragment = AccountMediaFragment() val fragment = AccountMediaFragment()
val args = Bundle() val args = Bundle()
args.putString(ACCOUNT_ID_ARG, accountId) args.putString(ACCOUNT_ID_ARG, accountId)
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
fragment.arguments = args fragment.arguments = args
return fragment return fragment
} }
private const val ACCOUNT_ID_ARG = "account_id" private const val ACCOUNT_ID_ARG = "account_id"
private const val TAG = "AccountMediaFragment" 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 @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
@ -78,6 +83,8 @@ class AccountMediaFragment : BaseFragment(), Injectable {
private var fetchingStatus = FetchingStatus.NOT_FETCHING private var fetchingStatus = FetchingStatus.NOT_FETCHING
private var isVisibleToUser: Boolean = false private var isVisibleToUser: Boolean = false
private var accountId: String?=null
private val callback = object : Callback<List<Status>> { private val callback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) { override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
@ -85,6 +92,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
topProgressBar?.hide()
statusView.show() statusView.show()
if (t is IOException) { if (t is IOException) {
statusView.setup(R.drawable.elephant_offline, R.string.error_network) { statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
@ -105,6 +113,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
topProgressBar?.hide()
val body = response.body() val body = response.body()
body?.let { fetched -> body?.let { fetched ->
@ -115,6 +124,8 @@ class AccountMediaFragment : BaseFragment(), Injectable {
result.addAll(AttachmentViewData.list(status)) result.addAll(AttachmentViewData.list(status))
} }
adapter.addTop(result) adapter.addTop(result)
if (result.isNotEmpty())
recyclerView.scrollToPosition(0)
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
statusView.show() 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?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false) return inflater.inflate(R.layout.fragment_timeline, container, false)
@ -171,24 +187,15 @@ class AccountMediaFragment : BaseFragment(), Injectable {
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter recyclerView.adapter = adapter
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
swipeRefreshLayout.setOnRefreshListener {
statusView.hide() if (isSwipeToRefreshEnabled) {
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener swipeRefreshLayout.setOnRefreshListener {
currentCall = if (statuses.isEmpty()) { refresh()
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) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
} }
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
statusView.visibility = View.GONE statusView.visibility = View.GONE
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@ -212,6 +219,22 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isVisibleToUser) doInitialLoadingIfNeeded() 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 // That's sort of an optimization to only load media once user has opened the tab
// Attention: can be called before *any* lifecycle method! // Attention: can be called before *any* lifecycle method!
override fun setUserVisibleHint(isVisibleToUser: Boolean) { override fun setUserVisibleHint(isVisibleToUser: Boolean) {
@ -224,12 +247,14 @@ class AccountMediaFragment : BaseFragment(), Injectable {
if (isAdded) { if (isAdded) {
statusView.hide() statusView.hide()
} }
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) currentCall = api.accountStatuses(accountId, null, null, null, null, true, null)
currentCall?.enqueue(callback) currentCall?.enqueue(callback)
} }
else if (needToRefresh)
refresh()
needToRefresh = false
} }
private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) { 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.Poll;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.RefreshableFragment;
import com.keylesspalace.tusky.interfaces.ReselectableFragment; import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
@ -82,6 +83,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.arch.core.util.Function; import androidx.arch.core.util.Function;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.AsyncDifferConfig; import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer; import androidx.recyclerview.widget.AsyncListDiffer;
@ -92,6 +94,7 @@ import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
@ -108,12 +111,15 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
public class TimelineFragment extends SFragment implements public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener, StatusActionListener,
Injectable, ReselectableFragment { Injectable, ReselectableFragment, RefreshableFragment {
private static final String TAG = "TimelineF"; // logging tag private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind"; private static final String KIND_ARG = "kind";
private static final String HASHTAG_OR_ID_ARG = "hashtag_or_id"; 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 static final int LOAD_AT_ONCE = 30;
private boolean isSwipeToRefreshEnabled = true;
private boolean isNeedRefresh;
public enum Kind { public enum Kind {
HOME, HOME,
@ -146,6 +152,7 @@ public class TimelineFragment extends SFragment implements
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private ProgressBar progressBar; private ProgressBar progressBar;
private ContentLoadingProgressBar topProgressBar;
private BackgroundMessageView statusView; private BackgroundMessageView statusView;
private TimelineAdapter adapter; private TimelineAdapter adapter;
@ -182,18 +189,19 @@ public class TimelineFragment extends SFragment implements
}); });
public static TimelineFragment newInstance(Kind kind) { public static TimelineFragment newInstance(Kind kind) {
TimelineFragment fragment = new TimelineFragment(); return newInstance(kind, null);
Bundle arguments = new Bundle();
arguments.putString(KIND_ARG, kind.name());
fragment.setArguments(arguments);
return fragment;
} }
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(); TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putString(KIND_ARG, kind.name()); arguments.putString(KIND_ARG, kind.name());
arguments.putString(HASHTAG_OR_ID_ARG, hashtagOrId); arguments.putString(HASHTAG_OR_ID_ARG, hashtagOrId);
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh);
fragment.setArguments(arguments); fragment.setArguments(arguments);
return fragment; return fragment;
} }
@ -213,6 +221,8 @@ public class TimelineFragment extends SFragment implements
adapter = new TimelineAdapter(dataSource, this); adapter = new TimelineAdapter(dataSource, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
} }
@Override @Override
@ -224,6 +234,7 @@ public class TimelineFragment extends SFragment implements
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
progressBar = rootView.findViewById(R.id.progressBar); progressBar = rootView.findViewById(R.id.progressBar);
statusView = rootView.findViewById(R.id.statusView); statusView = rootView.findViewById(R.id.statusView);
topProgressBar = rootView.findViewById(R.id.topProgressBar);
setupSwipeRefreshLayout(); setupSwipeRefreshLayout();
setupRecyclerView(); setupRecyclerView();
@ -236,6 +247,8 @@ public class TimelineFragment extends SFragment implements
this.sendInitialRequest(); this.sendInitialRequest();
} else { } else {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
if (isNeedRefresh)
onRefresh();
} }
return rootView; return rootView;
@ -388,11 +401,14 @@ public class TimelineFragment extends SFragment implements
} }
private void setupSwipeRefreshLayout() { private void setupSwipeRefreshLayout() {
Context context = swipeRefreshLayout.getContext(); swipeRefreshLayout.setEnabled(isSwipeToRefreshEnabled);
swipeRefreshLayout.setOnRefreshListener(this); if (isSwipeToRefreshEnabled) {
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); Context context = swipeRefreshLayout.getContext();
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, swipeRefreshLayout.setOnRefreshListener(this);
android.R.attr.colorBackground)); swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context,
android.R.attr.colorBackground));
}
} }
private void setupRecyclerView() { private void setupRecyclerView() {
@ -524,8 +540,10 @@ public class TimelineFragment extends SFragment implements
@Override @Override
public void onRefresh() { public void onRefresh() {
swipeRefreshLayout.setEnabled(true); if (isSwipeToRefreshEnabled)
swipeRefreshLayout.setEnabled(true);
this.statusView.setVisibility(View.GONE); this.statusView.setVisibility(View.GONE);
isNeedRefresh = false;
if (this.initialUpdateFailed) { if (this.initialUpdateFailed) {
updateCurrent(); updateCurrent();
} else { } else {
@ -936,6 +954,9 @@ public class TimelineFragment extends SFragment implements
private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId, private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId,
@Nullable String sinceIdMinusOne, @Nullable String sinceIdMinusOne,
final FetchEnd fetchEnd, final int pos) { 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) { if (kind == Kind.HOME) {
TimelineRequestMode mode; TimelineRequestMode mode;
// allow getting old statuses/fallbacks for network only for for bottom loading // allow getting old statuses/fallbacks for network only for for bottom loading
@ -1015,20 +1036,24 @@ public class TimelineFragment extends SFragment implements
break; break;
} }
} }
updateBottomLoadingState(fetchEnd); if (isAdded()) {
progressBar.setVisibility(View.GONE); topProgressBar.hide();
swipeRefreshLayout.setRefreshing(false); updateBottomLoadingState(fetchEnd);
swipeRefreshLayout.setEnabled(true); progressBar.setVisibility(View.GONE);
if (this.statuses.size() == 0) { swipeRefreshLayout.setRefreshing(false);
this.showNothing(); swipeRefreshLayout.setEnabled(true);
} else { if (this.statuses.size() == 0) {
this.statusView.setVisibility(View.GONE); this.showNothing();
} else {
this.statusView.setVisibility(View.GONE);
}
} }
} }
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
if (isAdded()) { if (isAdded()) {
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
topProgressBar.hide();
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
Placeholder placeholder = statuses.get(position).asLeftOrNull(); Placeholder placeholder = statuses.get(position).asLeftOrNull();
@ -1267,7 +1292,10 @@ public class TimelineFragment extends SFragment implements
adapter.notifyItemRangeInserted(position, count); adapter.notifyItemRangeInserted(position, count);
Context context = getContext(); Context context = getContext();
if (position == 0 && context != null) { if (position == 0 && context != null) {
recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); 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() { public void onReselect() {
jumpToTop(); 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.AccountMediaFragment;
import com.keylesspalace.tusky.fragment.TimelineFragment; 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.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -34,6 +38,8 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
private SparseArray<Fragment> fragments = new SparseArray<>(TAB_COUNT); private SparseArray<Fragment> fragments = new SparseArray<>(TAB_COUNT);
private final Set<Integer> pagesToRefresh = new HashSet<>();
public AccountPagerAdapter(FragmentManager manager, String accountId) { public AccountPagerAdapter(FragmentManager manager, String accountId) {
super(manager); super(manager);
this.accountId = accountId; this.accountId = accountId;
@ -48,16 +54,16 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
public Fragment getItem(int position) { public Fragment getItem(int position) {
switch (position) { switch (position) {
case 0: { case 0: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId); return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId,false);
} }
case 1: { case 1: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId); return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId,false);
} }
case 2: { case 2: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId); return TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId,false);
} }
case 3: { case 3: {
return AccountMediaFragment.newInstance(accountId); return AccountMediaFragment.newInstance(accountId,false);
} }
default: { default: {
throw new AssertionError("Page " + position + " is out of AccountPagerAdapter bounds"); 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); Object fragment = super.instantiateItem(container, position);
if (fragment instanceof Fragment) if (fragment instanceof Fragment)
fragments.put(position, (Fragment) fragment); fragments.put(position, (Fragment) fragment);
if (pagesToRefresh.contains(position)) {
if (fragment instanceof RefreshableFragment)
((RefreshableFragment) fragment).refreshContent();
pagesToRefresh.remove(position);
}
return fragment; return fragment;
} }
@ -94,4 +105,16 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
public Fragment getFragment(int position) { public Fragment getFragment(int position) {
return fragments.get(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.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
@ -16,30 +17,36 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import javax.inject.Inject import javax.inject.Inject
class AccountViewModel @Inject constructor( class AccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val eventHub: EventHub private val eventHub: EventHub,
): ViewModel() { private val accountManager: AccountManager
) : ViewModel() {
val accountData = MutableLiveData<Resource<Account>>() val accountData = MutableLiveData<Resource<Account>>()
val relationshipData = MutableLiveData<Resource<Relationship>>() val relationshipData = MutableLiveData<Resource<Relationship>>()
private val callList: MutableList<Call<*>> = mutableListOf() private val callList: MutableList<Call<*>> = mutableListOf()
private val disposable: Disposable = eventHub.events private val disposable: Disposable = eventHub.events
.subscribe { event -> .subscribe { event ->
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
accountData.postValue(Success(event.newProfileData)) accountData.postValue(Success(event.newProfileData))
}
} }
}
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
private var isDataLoading = false
fun obtainAccount(accountId: String, reload: Boolean = false) { lateinit var accountId: String
if(accountData.value == null || reload) { var isSelf = false
private fun obtainAccount(reload: Boolean = false) {
if (accountData.value == null || reload) {
isDataLoading = true
accountData.postValue(Loading()) accountData.postValue(Loading())
val call = mastodonApi.account(accountId) val call = mastodonApi.account(accountId)
call.enqueue(object : Callback<Account> { call.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, override fun onResponse(call: Call<Account>,
response: Response<Account>) { response: Response<Account>) {
if (response.isSuccessful) { if (response.isSuccessful) {
@ -47,10 +54,14 @@ class AccountViewModel @Inject constructor(
} else { } else {
accountData.postValue(Error()) accountData.postValue(Error())
} }
isDataLoading = false
isRefreshing.postValue(false)
} }
override fun onFailure(call: Call<Account>, t: Throwable) { override fun onFailure(call: Call<Account>, t: Throwable) {
accountData.postValue(Error()) accountData.postValue(Error())
isDataLoading = false
isRefreshing.postValue(false)
} }
}) })
@ -58,14 +69,14 @@ class AccountViewModel @Inject constructor(
} }
} }
fun obtainRelationship(accountId: String, reload: Boolean = false) { private fun obtainRelationship(reload: Boolean = false) {
if(relationshipData.value == null || reload) { if (relationshipData.value == null || reload) {
relationshipData.postValue(Loading()) relationshipData.postValue(Loading())
val ids = listOf(accountId) val ids = listOf(accountId)
val call = mastodonApi.relationships(ids) val call = mastodonApi.relationships(ids)
call.enqueue(object : Callback<List<Relationship>> { call.enqueue(object : Callback<List<Relationship>> {
override fun onResponse(call: Call<List<Relationship>>, override fun onResponse(call: Call<List<Relationship>>,
response: Response<List<Relationship>>) { response: Response<List<Relationship>>) {
val relationships = response.body() val relationships = response.body()
@ -86,47 +97,47 @@ class AccountViewModel @Inject constructor(
} }
} }
fun changeFollowState(id: String) { fun changeFollowState() {
val relationship = relationshipData.value?.data val relationship = relationshipData.value?.data
if (relationship?.following == true || relationship?.requested == true) { if (relationship?.following == true || relationship?.requested == true) {
changeRelationship(RelationShipAction.UNFOLLOW, id) changeRelationship(RelationShipAction.UNFOLLOW)
} else { } else {
changeRelationship(RelationShipAction.FOLLOW, id) changeRelationship(RelationShipAction.FOLLOW)
} }
} }
fun changeBlockState(id: String) { fun changeBlockState() {
if (relationshipData.value?.data?.blocking == true) { if (relationshipData.value?.data?.blocking == true) {
changeRelationship(RelationShipAction.UNBLOCK, id) changeRelationship(RelationShipAction.UNBLOCK)
} else { } else {
changeRelationship(RelationShipAction.BLOCK, id) changeRelationship(RelationShipAction.BLOCK)
} }
} }
fun changeMuteState(id: String) { fun changeMuteState() {
if (relationshipData.value?.data?.muting == true) { if (relationshipData.value?.data?.muting == true) {
changeRelationship(RelationShipAction.UNMUTE, id) changeRelationship(RelationShipAction.UNMUTE)
} else { } else {
changeRelationship(RelationShipAction.MUTE, id) changeRelationship(RelationShipAction.MUTE)
} }
} }
fun changeShowReblogsState(id: String) { fun changeShowReblogsState() {
if (relationshipData.value?.data?.showingReblogs == true) { if (relationshipData.value?.data?.showingReblogs == true) {
changeRelationship(RelationShipAction.FOLLOW, id, false) changeRelationship(RelationShipAction.FOLLOW, false)
} else { } 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 relation = relationshipData.value?.data
val account = accountData.value?.data val account = accountData.value?.data
if(relation != null && account != null) { if (relation != null && account != null) {
// optimistically post new state for faster response // optimistically post new state for faster response
val newRelation = when(relationshipAction) { val newRelation = when (relationshipAction) {
RelationShipAction.FOLLOW -> { RelationShipAction.FOLLOW -> {
if (account.locked) { if (account.locked) {
relation.copy(requested = true) relation.copy(requested = true)
@ -134,11 +145,11 @@ class AccountViewModel @Inject constructor(
relation.copy(following = true) relation.copy(following = true)
} }
} }
RelationShipAction.UNFOLLOW -> relation.copy(following = false) RelationShipAction.UNFOLLOW -> relation.copy(following = false)
RelationShipAction.BLOCK -> relation.copy(blocking = true) RelationShipAction.BLOCK -> relation.copy(blocking = true)
RelationShipAction.UNBLOCK -> relation.copy(blocking = false) RelationShipAction.UNBLOCK -> relation.copy(blocking = false)
RelationShipAction.MUTE -> relation.copy(muting = true) RelationShipAction.MUTE -> relation.copy(muting = true)
RelationShipAction.UNMUTE -> relation.copy(muting = false) RelationShipAction.UNMUTE -> relation.copy(muting = false)
} }
relationshipData.postValue(Loading(newRelation)) relationshipData.postValue(Loading(newRelation))
} }
@ -151,10 +162,11 @@ class AccountViewModel @Inject constructor(
relationshipData.postValue(Success(relationship)) relationshipData.postValue(Success(relationship))
when (relationshipAction) { when (relationshipAction) {
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(id)) RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(id)) RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId))
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(id)) RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId))
else -> {} else -> {
}
} }
} else { } else {
@ -168,13 +180,13 @@ class AccountViewModel @Inject constructor(
} }
} }
val call = when(relationshipAction) { val call = when (relationshipAction) {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(id, showReblogs) RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs)
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(id) RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
RelationShipAction.BLOCK -> mastodonApi.blockAccount(id) RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(id) RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
RelationShipAction.MUTE -> mastodonApi.muteAccount(id) RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId)
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(id) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
} }
call.enqueue(callback) call.enqueue(callback)
@ -189,6 +201,27 @@ class AccountViewModel @Inject constructor(
disposable.dispose() 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 { enum class RelationShipAction {
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE
} }

View file

@ -45,5 +45,16 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error" tools:src="@drawable/elephant_error"
tools:visibility="visible" /> 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> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </FrameLayout>

View file

@ -1,316 +1,367 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accountCoordinatorLayout" android:id="@+id/swipeToRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:textDirection="anyRtl"
android:fillViewport="true">
<com.google.android.material.appbar.AppBarLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/accountAppBarLayout" android:id="@+id/accountCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="?attr/toolbar_background_color"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?android:attr/colorBackground"
app:titleEnabled="false">
<ImageView
android:id="@+id/accountHeaderImageView"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_alignTop="@+id/account_header_info"
android:background="?attr/account_header_background_color"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#000" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/accountHeaderInfoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="180dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<Button
android:id="@+id/accountFollowButton"
style="@style/TuskyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Follow" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/accountDisplayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="62dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Tusky Mastodon Client " />
<TextView
android:id="@+id/accountUsernameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountDisplayNameTextView"
tools:text="\@Tusky" />
<ImageView
android:id="@+id/accountLockedImageView"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/accountUsernameTextView"
app:layout_constraintStart_toEndOf="@+id/accountUsernameTextView"
app:layout_constraintTop_toTopOf="@+id/accountUsernameTextView"
app:srcCompat="@drawable/reblog_private_light"
tools:visibility="visible" />
<TextView
android:id="@+id/accountFollowsYouTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/follows_you"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
tools:visibility="visible" />
<TextView
android:id="@+id/accountBadgeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/profile_badge_bot_text"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/accountFollowsYouTextView"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
app:layout_goneMarginStart="0dp"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/labelBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
<androidx.emoji.widget.EmojiTextView
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"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
tools:text="This is a test description. Descriptions can be quite looooong." />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountFieldList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/accountNoteTextView"
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"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:lineSpacingMultiplier="1.1"
android:text="@string/label_remote_account"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/accountMovedView"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/accountStatuses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
<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"
tools:text="3000" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_statuses"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountFollowing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowers"
app:layout_constraintStart_toEndOf="@id/accountStatuses"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
<TextView
android:id="@+id/accountFollowingTextView"
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"
tools:text="500" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_follows"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountFollowers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/accountFollowing"
app:layout_constraintTop_toBottomOf="@id/accountRemoveView">
<TextView
android:id="@+id/accountFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:color/transparent"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
tools:text="1234" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_followers"
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 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/accountToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/accountTabLayout"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
app:tabGravity="center"
app:tabMode="scrollable"
app:tabTextAppearance="@style/TuskyTabAppearance" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/accountFragmentViewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> android:fillViewport="true"
android:textDirection="anyRtl">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.appbar.AppBarLayout
android:id="@+id/accountFloatingActionButton" android:id="@+id/accountAppBarLayout"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:elevation="@dimen/actionbar_elevation">
android:layout_margin="16dp"
android:contentDescription="@string/action_mention"
app:srcCompat="@drawable/ic_create_24dp" />
<include layout="@layout/item_status_bottom_sheet" /> <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="?attr/toolbar_background_color"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?android:attr/colorBackground"
app:titleEnabled="false">
<com.keylesspalace.tusky.view.RoundedImageView <ImageView
android:id="@+id/accountAvatarImageView" android:id="@+id/accountHeaderImageView"
android:layout_width="@dimen/account_activity_avatar_size" android:layout_width="match_parent"
android:layout_height="@dimen/account_activity_avatar_size" android:layout_height="180dp"
android:layout_marginStart="16dp" android:layout_alignTop="@+id/account_header_info"
android:background="@drawable/avatar_background" android:background="?attr/account_header_background_color"
android:padding="3dp" android:scaleType="centerCrop"
app:layout_anchor="@+id/accountHeaderInfoContainer" app:layout_collapseMode="parallax"
app:layout_anchorGravity="top" app:layout_constraintStart_toStartOf="parent"
app:layout_scrollFlags="scroll" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/avatar_default" /> tools:background="#000" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/accountHeaderInfoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="180dp"
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 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"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="62dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Tusky Mastodon Client " />
<TextView
android:id="@+id/accountUsernameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountDisplayNameTextView"
tools:text="\@Tusky" />
<ImageView
android:id="@+id/accountLockedImageView"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/accountUsernameTextView"
app:layout_constraintStart_toEndOf="@+id/accountUsernameTextView"
app:layout_constraintTop_toTopOf="@+id/accountUsernameTextView"
app:srcCompat="@drawable/reblog_private_light"
tools:visibility="visible" />
<TextView
android:id="@+id/accountFollowsYouTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/follows_you"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
tools:visibility="visible" />
<TextView
android:id="@+id/accountBadgeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/profile_badge_bot_text"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/accountFollowsYouTextView"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
app:layout_goneMarginStart="0dp"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/labelBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
<androidx.emoji.widget.EmojiTextView
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"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
tools:text="This is a test description. Descriptions can be quite looooong." />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountFieldList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/accountNoteTextView"
tools:itemCount="2"
tools:listitem="@layout/item_account_field" />
<TextView
android:id="@+id/accountRemoveView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:lineSpacingMultiplier="1.1"
android:text="@string/label_remote_account"
android:visibility="gone"
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"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
app:layout_constraintStart_toStartOf="parent"
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"
tools:text="3000" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_statuses"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountFollowing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowers"
app:layout_constraintStart_toEndOf="@id/accountStatuses"
app:layout_constraintTop_toBottomOf="@id/barrierRemote">
<TextView
android:id="@+id/accountFollowingTextView"
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"
tools:text="500" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_follows"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountFollowers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/accountFollowing"
app:layout_constraintTop_toBottomOf="@id/barrierRemote">
<TextView
android:id="@+id/accountFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:color/transparent"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
tools:text="1234" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_followers"
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 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/accountToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/accountTabLayout"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
app:tabGravity="center"
app:tabMode="scrollable"
app:tabTextAppearance="@style/TuskyTabAppearance" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/accountFragmentViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/accountFloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/action_mention"
app:srcCompat="@drawable/ic_create_24dp" />
<include layout="@layout/item_status_bottom_sheet" />
<com.keylesspalace.tusky.view.RoundedImageView
android:id="@+id/accountAvatarImageView"
android:layout_width="@dimen/account_activity_avatar_size"
android:layout_height="@dimen/account_activity_avatar_size"
android:layout_marginStart="16dp"
android:background="@drawable/avatar_background"
android:padding="3dp"
app:layout_anchor="@+id/accountHeaderInfoContainer"
app:layout_anchorGravity="top"
app:layout_scrollFlags="scroll"
app:srcCompat="@drawable/avatar_default" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View file

@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -38,5 +37,15 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error" tools:src="@drawable/elephant_error"
tools:visibility="visible"/> 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> </androidx.constraintlayout.widget.ConstraintLayout>