From ae5d8b86330567c436ff27e2f2949e64eb571bba Mon Sep 17 00:00:00 2001 From: pandasoft0 Date: Wed, 15 May 2019 13:43:16 +0300 Subject: [PATCH] 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 --- .../keylesspalace/tusky/AccountActivity.kt | 459 +++++++----- .../tusky/FavouritesActivity.java | 2 + .../tusky/ModalTimelineActivity.kt | 1 + .../keylesspalace/tusky/ViewTagActivity.java | 2 + .../keylesspalace/tusky/di/NetworkModule.kt | 2 +- .../tusky/fragment/AccountMediaFragment.kt | 72 +- .../tusky/fragment/TimelineFragment.java | 80 ++- .../tusky/interfaces/RefreshableFragment.kt | 11 + .../tusky/pager/AccountPagerAdapter.java | 31 +- .../tusky/viewmodel/AccountViewModel.kt | 123 ++-- .../res/layout-sw640dp/fragment_timeline.xml | 11 + app/src/main/res/layout/activity_account.xml | 663 ++++++++++-------- app/src/main/res/layout/fragment_timeline.xml | 13 +- 13 files changed, 890 insertions(+), 580 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 05a51454..e419375f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -64,7 +64,7 @@ import javax.inject.Inject class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector, LinkListener { @Inject - lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector @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> { - 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() + // Obtain information to fill out the profile. + viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)) + + if (viewModel.isSelf) { + updateButtons() + } + + 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> { - val relation = it?.data - if (relation != null) { - onRelationshipChanged(relation) - } - if (it is Error) { - Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry) { reload() } - .show() - } + 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) + 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 + } - 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() + /** + * Subscribe to data loaded at the view model + */ + private fun subscribeObservables() { + viewModel.accountData.observe(this, Observer> { + 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> { + val relation = it?.data + if (relation != null) { + onRelationshipChanged(relation) + } - override fun onTabUnselected(tab: TabLayout.Tab?) {} - - 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 - 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 - } + loadedAccount = account ?: return - 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) + val usernameFormatted = getString(R.string.status_username_format, account.username) + accountUsernameTextView.text = usernameFormatted + accountDisplayNameTextView.text = CustomEmojiHelper.emojifyString(account.name, account.emojis, accountDisplayNameTextView) - accountLockedImageView.visible(account.locked) - accountBadgeTextView.visible(account.bot) + 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,52 +397,75 @@ 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(R.id.accountMovedView).setOnClickListener { - 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() + // necessary because accountMovedView is now replaced in layout hierachy + findViewById(R.id.accountMovedViewLayout).setOnClickListener { + 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) + } + + } + + /** + * 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 } + + if (blocking) { + viewModel.changeBlockState() + return@setOnClickListener + } + when (followState) { - AccountActivity.FollowState.NOT_FOLLOWING -> { - viewModel.changeFollowState(accountId) + FollowState.NOT_FOLLOWING -> { + viewModel.changeFollowState() } - AccountActivity.FollowState.REQUESTED -> { + 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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java index d3b4b0f8..643a221c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index ebca7b64..cbb115d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -30,6 +30,7 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasSu } } + @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 9653e8ad..d0ed9acf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index bdbe7049..652c61cb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -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() diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index fb020694..db6afa2f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -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> { override fun onFailure(call: Call>?, 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) - 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) + + if (isSwipeToRefreshEnabled) { + swipeRefreshLayout.setOnRefreshListener { + refresh() } - 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 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, currentIndex: Int, view: View?) { @@ -321,4 +346,13 @@ class AccountMediaFragment : BaseFragment(), Injectable { } } } + + override fun refreshContent() { + if (isAdded) + refresh() + else + needToRefresh = true + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 5fbd5afb..46ceb086 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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,11 +401,14 @@ public class TimelineFragment extends SFragment implements } private void setupSwipeRefreshLayout() { - Context context = swipeRefreshLayout.getContext(); - swipeRefreshLayout.setOnRefreshListener(this); - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); - swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, - android.R.attr.colorBackground)); + 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() { @@ -524,8 +540,10 @@ public class TimelineFragment extends SFragment implements @Override public void onRefresh() { - swipeRefreshLayout.setEnabled(true); + 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,20 +1036,24 @@ public class TimelineFragment extends SFragment implements break; } } - updateBottomLoadingState(fetchEnd); - progressBar.setVisibility(View.GONE); - swipeRefreshLayout.setRefreshing(false); - swipeRefreshLayout.setEnabled(true); - if (this.statuses.size() == 0) { - this.showNothing(); - } else { - this.statusView.setVisibility(View.GONE); + if (isAdded()) { + topProgressBar.hide(); + updateBottomLoadingState(fetchEnd); + progressBar.setVisibility(View.GONE); + swipeRefreshLayout.setRefreshing(false); + swipeRefreshLayout.setEnabled(true); + if (this.statuses.size() == 0) { + this.showNothing(); + } else { + 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) { - 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() { jumpToTop(); } + + @Override + public void refreshContent() { + if (isAdded()) + onRefresh(); + else + isNeedRefresh = true; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt new file mode 100644 index 00000000..5032774f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java index 1fcb3f81..de2f0523 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java @@ -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 fragments = new SparseArray<>(TAB_COUNT); + private final Set 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>() val relationshipData = MutableLiveData>() private val callList: MutableList> = mutableListOf() private val disposable: Disposable = eventHub.events - .subscribe { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { - accountData.postValue(Success(event.newProfileData)) - } + .subscribe { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { + accountData.postValue(Success(event.newProfileData)) } + } + val isRefreshing = MutableLiveData().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) - call.enqueue(object : Callback { + call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { @@ -47,10 +54,14 @@ class AccountViewModel @Inject constructor( } else { accountData.postValue(Error()) } + isDataLoading = false + isRefreshing.postValue(false) } override fun onFailure(call: Call, t: Throwable) { accountData.postValue(Error()) + isDataLoading = false + isRefreshing.postValue(false) } }) @@ -58,14 +69,14 @@ 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()) val ids = listOf(accountId) val call = mastodonApi.relationships(ids) - call.enqueue(object : Callback> { + call.enqueue(object : Callback> { override fun onResponse(call: Call>, response: Response>) { val relationships = response.body() @@ -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) @@ -134,11 +145,11 @@ class AccountViewModel @Inject constructor( relation.copy(following = true) } } - RelationShipAction.UNFOLLOW -> relation.copy(following = false) - RelationShipAction.BLOCK -> relation.copy(blocking = true) - RelationShipAction.UNBLOCK -> relation.copy(blocking = false) - RelationShipAction.MUTE -> relation.copy(muting = true) - RelationShipAction.UNMUTE -> relation.copy(muting = false) + RelationShipAction.UNFOLLOW -> relation.copy(following = false) + RelationShipAction.BLOCK -> relation.copy(blocking = true) + RelationShipAction.UNBLOCK -> relation.copy(blocking = false) + RelationShipAction.MUTE -> relation.copy(muting = true) + RelationShipAction.UNMUTE -> relation.copy(muting = false) } relationshipData.postValue(Loading(newRelation)) } @@ -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 } diff --git a/app/src/main/res/layout-sw640dp/fragment_timeline.xml b/app/src/main/res/layout-sw640dp/fragment_timeline.xml index 2fe2d337..27e983e5 100644 --- a/app/src/main/res/layout-sw640dp/fragment_timeline.xml +++ b/app/src/main/res/layout-sw640dp/fragment_timeline.xml @@ -45,5 +45,16 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/elephant_error" tools:visibility="visible" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 2b8aacdf..22d1d124 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -1,316 +1,367 @@ - + android:layout_height="match_parent"> - - - - - - - - -