ff8dd37855
* Replace "warn"-filtered posts in timelines and thread view with placeholders * Adapt hashtag muting interface * Rework filter UI * Add icon for account preferences * Clean up UI * WIP: Use chips instead of a list. Adjust padding * Scroll the filter edit activity Nested scrolling views (e.g., an activity that scrolls with an embedded list that also scrolls) can be difficult UI. Since the list of contexts is fixed, replace it with a fixed collection of switches, so there's no need to scroll the list. Since the list of actions is only two (warn, hide), and are mutually exclusive, replace the spinner with two radio buttons. Use the accent colour and title styles on the different heading titles in the layout, to match the presentation in Preferences. Add an explicit "Cancel" button. The layout is a straightforward LinearLayout, so use that instead of ConstraintLayout, and remove some unncessary IDs. Update EditFilterActivity to handle the new layout. * Cleanup * Add more information to the filter list view * First pass on code review comments * Add view model to filters activity * Add view model to edit filters activity * Only use the status wrapper for filtered statuses * Relint --------- Co-authored-by: Nik Clayton <nik@ngo.org.uk>
643 lines
26 KiB
Kotlin
643 lines
26 KiB
Kotlin
/* Copyright 2021 Tusky Contributors
|
|
*
|
|
* This file is a part of Tusky.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
* Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
package com.keylesspalace.tusky.components.timeline
|
|
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import android.view.LayoutInflater
|
|
import android.view.Menu
|
|
import android.view.MenuInflater
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.accessibility.AccessibilityManager
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.view.MenuProvider
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.paging.LoadState
|
|
import androidx.preference.PreferenceManager
|
|
import androidx.recyclerview.widget.DividerItemDecoration
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
|
import at.connyduck.sparkbutton.helpers.Utils
|
|
import autodispose2.androidx.lifecycle.autoDispose
|
|
import com.google.android.material.color.MaterialColors
|
|
import com.keylesspalace.tusky.BaseActivity
|
|
import com.keylesspalace.tusky.R
|
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
|
import com.keylesspalace.tusky.appstore.StatusEditedEvent
|
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent
|
|
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
|
|
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
|
import com.keylesspalace.tusky.di.Injectable
|
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
import com.keylesspalace.tusky.entity.Status
|
|
import com.keylesspalace.tusky.fragment.SFragment
|
|
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.settings.PrefKeys
|
|
import com.keylesspalace.tusky.util.CardViewMode
|
|
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
|
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
|
import com.keylesspalace.tusky.util.hide
|
|
import com.keylesspalace.tusky.util.show
|
|
import com.keylesspalace.tusky.util.unsafeLazy
|
|
import com.keylesspalace.tusky.util.viewBinding
|
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
|
import com.mikepenz.iconics.IconicsDrawable
|
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|
import com.mikepenz.iconics.utils.colorInt
|
|
import com.mikepenz.iconics.utils.sizeDp
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
import io.reactivex.rxjava3.core.Observable
|
|
import kotlinx.coroutines.flow.collectLatest
|
|
import kotlinx.coroutines.launch
|
|
import java.io.IOException
|
|
import java.util.concurrent.TimeUnit
|
|
import javax.inject.Inject
|
|
|
|
class TimelineFragment :
|
|
SFragment(),
|
|
OnRefreshListener,
|
|
StatusActionListener,
|
|
Injectable,
|
|
ReselectableFragment,
|
|
RefreshableFragment,
|
|
MenuProvider {
|
|
|
|
@Inject
|
|
lateinit var viewModelFactory: ViewModelFactory
|
|
|
|
@Inject
|
|
lateinit var eventHub: EventHub
|
|
|
|
private val viewModel: TimelineViewModel by unsafeLazy {
|
|
if (kind == TimelineViewModel.Kind.HOME) {
|
|
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
|
} else {
|
|
ViewModelProvider(this, viewModelFactory)[NetworkTimelineViewModel::class.java]
|
|
}
|
|
}
|
|
|
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
|
|
|
private lateinit var kind: TimelineViewModel.Kind
|
|
|
|
private lateinit var adapter: TimelinePagingAdapter
|
|
|
|
private var isSwipeToRefreshEnabled = true
|
|
private var hideFab = false
|
|
|
|
/**
|
|
* Adapter position of the placeholder that was most recently clicked to "Load more". If null
|
|
* then there is no active "Load more" operation
|
|
*/
|
|
private var loadMorePosition: Int? = null
|
|
|
|
/** ID of the status immediately below the most recent "Load more" placeholder click */
|
|
// The Paging library assumes that the user will be scrolling down a list of items,
|
|
// and if new items are loaded but not visible then it's reasonable to scroll to the top
|
|
// of the inserted items. It does not seem to be possible to disable that behaviour.
|
|
//
|
|
// That behaviour should depend on the user's preferred reading order. If they prefer to
|
|
// read oldest first then the list should be scrolled to the bottom of the freshly
|
|
// inserted statuses.
|
|
//
|
|
// To do this:
|
|
//
|
|
// 1. When "Load more" is clicked (onLoadMore()):
|
|
// a. Remember the adapter position of the "Load more" item in loadMorePosition
|
|
// b. Remember the ID of the status immediately below the "Load more" item in
|
|
// statusIdBelowLoadMore
|
|
// 2. After the new items have been inserted, search the adapter for the position of the
|
|
// status with id == statusIdBelowLoadMore.
|
|
// 3. If this position is still visible on screen then do nothing, otherwise, scroll the view
|
|
// so that the status is visible.
|
|
//
|
|
// The user can then scroll up to read the new statuses.
|
|
private var statusIdBelowLoadMore: String? = null
|
|
|
|
/** The user's preferred reading order */
|
|
private lateinit var readingOrder: ReadingOrder
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
val arguments = requireArguments()
|
|
kind = TimelineViewModel.Kind.valueOf(arguments.getString(KIND_ARG)!!)
|
|
val id: String? = if (kind == TimelineViewModel.Kind.USER ||
|
|
kind == TimelineViewModel.Kind.USER_PINNED ||
|
|
kind == TimelineViewModel.Kind.USER_WITH_REPLIES ||
|
|
kind == TimelineViewModel.Kind.LIST
|
|
) {
|
|
arguments.getString(ID_ARG)!!
|
|
} else {
|
|
null
|
|
}
|
|
|
|
val tags = if (kind == TimelineViewModel.Kind.TAG) {
|
|
arguments.getStringArrayList(HASHTAGS_ARG)!!
|
|
} else {
|
|
listOf()
|
|
}
|
|
viewModel.init(
|
|
kind,
|
|
id,
|
|
tags,
|
|
)
|
|
|
|
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null))
|
|
|
|
val statusDisplayOptions = StatusDisplayOptions(
|
|
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
|
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
|
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
|
|
showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
|
|
useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
|
|
cardViewMode = if (preferences.getBoolean(
|
|
PrefKeys.SHOW_CARDS_IN_TIMELINES,
|
|
false
|
|
)
|
|
) CardViewMode.INDENTED else CardViewMode.NONE,
|
|
confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true),
|
|
confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false),
|
|
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
|
showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
|
|
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
|
)
|
|
adapter = TimelinePagingAdapter(
|
|
statusDisplayOptions,
|
|
this
|
|
)
|
|
}
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View? {
|
|
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
|
|
|
setupSwipeRefreshLayout()
|
|
setupRecyclerView()
|
|
|
|
adapter.addLoadStateListener { loadState ->
|
|
if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
|
|
binding.swipeRefreshLayout.isRefreshing = false
|
|
}
|
|
|
|
binding.statusView.hide()
|
|
binding.progressBar.hide()
|
|
|
|
if (adapter.itemCount == 0) {
|
|
when (loadState.refresh) {
|
|
is LoadState.NotLoading -> {
|
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
|
binding.statusView.show()
|
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
|
|
}
|
|
}
|
|
is LoadState.Error -> {
|
|
binding.statusView.show()
|
|
|
|
if ((loadState.refresh as LoadState.Error).error is IOException) {
|
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network)
|
|
} else {
|
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic)
|
|
}
|
|
}
|
|
is LoadState.Loading -> {
|
|
binding.progressBar.show()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
if (positionStart == 0 && adapter.itemCount != itemCount) {
|
|
binding.recyclerView.post {
|
|
if (getView() != null) {
|
|
if (isSwipeToRefreshEnabled) {
|
|
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
|
} else binding.recyclerView.scrollToPosition(0)
|
|
}
|
|
}
|
|
}
|
|
if (readingOrder == ReadingOrder.OLDEST_FIRST) {
|
|
updateReadingPositionForOldestFirst()
|
|
}
|
|
}
|
|
})
|
|
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
viewModel.statuses.collectLatest { pagingData ->
|
|
adapter.submitData(pagingData)
|
|
}
|
|
}
|
|
|
|
if (actionButtonPresent()) {
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
hideFab = preferences.getBoolean("fabHide", false)
|
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
|
val composeButton = (activity as ActionButtonActivity).actionButton
|
|
if (composeButton != null) {
|
|
if (hideFab) {
|
|
if (dy > 0 && composeButton.isShown) {
|
|
composeButton.hide() // hides the button if we're scrolling down
|
|
} else if (dy < 0 && !composeButton.isShown) {
|
|
composeButton.show() // shows it if we are scrolling up
|
|
}
|
|
} else if (!composeButton.isShown) {
|
|
composeButton.show()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
eventHub.events
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
|
.subscribe { event ->
|
|
when (event) {
|
|
is PreferenceChangedEvent -> {
|
|
onPreferenceChanged(event.preferenceKey)
|
|
}
|
|
is StatusComposedEvent -> {
|
|
val status = event.status
|
|
handleStatusComposeEvent(status)
|
|
}
|
|
is StatusEditedEvent -> {
|
|
handleStatusComposeEvent(event.status)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
if (isSwipeToRefreshEnabled) {
|
|
menuInflater.inflate(R.menu.fragment_timeline, menu)
|
|
menu.findItem(R.id.action_refresh)?.apply {
|
|
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
|
|
sizeDp = 20
|
|
colorInt =
|
|
MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
|
return when (menuItem.itemId) {
|
|
R.id.action_refresh -> {
|
|
if (isSwipeToRefreshEnabled) {
|
|
binding.swipeRefreshLayout.isRefreshing = true
|
|
|
|
refreshContent()
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the correct reading position in the timeline after the user clicked "Load more",
|
|
* assuming the reading position should be below the freshly-loaded statuses.
|
|
*/
|
|
// Note: The positionStart parameter to onItemRangeInserted() does not always
|
|
// match the adapter position where data was inserted (which is why loadMorePosition
|
|
// is tracked manually, see this bug report for another example:
|
|
// https://github.com/android/architecture-components-samples/issues/726).
|
|
private fun updateReadingPositionForOldestFirst() {
|
|
var position = loadMorePosition ?: return
|
|
val statusIdBelowLoadMore = statusIdBelowLoadMore ?: return
|
|
|
|
var status: StatusViewData?
|
|
while (adapter.peek(position).let { status = it; it != null }) {
|
|
if (status?.id == statusIdBelowLoadMore) {
|
|
val lastVisiblePosition =
|
|
(binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
|
if (position > lastVisiblePosition) {
|
|
binding.recyclerView.scrollToPosition(position)
|
|
}
|
|
break
|
|
}
|
|
position++
|
|
}
|
|
loadMorePosition = null
|
|
}
|
|
|
|
private fun setupSwipeRefreshLayout() {
|
|
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
|
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
|
}
|
|
|
|
private fun setupRecyclerView() {
|
|
binding.recyclerView.setAccessibilityDelegateCompat(
|
|
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
|
|
if (pos in 0 until adapter.itemCount) {
|
|
adapter.peek(pos)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
)
|
|
binding.recyclerView.setHasFixedSize(true)
|
|
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
|
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
|
binding.recyclerView.addItemDecoration(divider)
|
|
|
|
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
binding.recyclerView.adapter = adapter
|
|
}
|
|
|
|
override fun onRefresh() {
|
|
binding.statusView.hide()
|
|
|
|
adapter.refresh()
|
|
}
|
|
|
|
override fun onReply(position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
super.reply(status.status)
|
|
}
|
|
|
|
override fun onReblog(reblog: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.reblog(reblog, status)
|
|
}
|
|
|
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.favorite(favourite, status)
|
|
}
|
|
|
|
override fun onBookmark(bookmark: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.bookmark(bookmark, status)
|
|
}
|
|
|
|
override fun onVoteInPoll(position: Int, choices: List<Int>) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.voteInPoll(choices, status)
|
|
}
|
|
|
|
override fun clearWarningAction(position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.clearWarning(status)
|
|
}
|
|
|
|
override fun onMore(view: View, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
super.more(status.status, view, position)
|
|
}
|
|
|
|
override fun onOpenReblog(position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
super.openReblog(status.status)
|
|
}
|
|
|
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.changeExpanded(expanded, status)
|
|
}
|
|
|
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.changeContentShowing(isShowing, status)
|
|
}
|
|
|
|
override fun onShowReblogs(position: Int) {
|
|
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
|
|
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
|
|
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
|
}
|
|
|
|
override fun onShowFavs(position: Int) {
|
|
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
|
|
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
|
|
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
|
}
|
|
|
|
override fun onLoadMore(position: Int) {
|
|
val placeholder = adapter.peek(position)?.asPlaceholderOrNull() ?: return
|
|
loadMorePosition = position
|
|
statusIdBelowLoadMore = adapter.peek(position + 1)?.id
|
|
viewModel.loadMore(placeholder.id)
|
|
}
|
|
|
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.changeContentCollapsed(isCollapsed, status)
|
|
}
|
|
|
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
super.viewMedia(
|
|
attachmentIndex,
|
|
AttachmentViewData.list(status.actionable),
|
|
view
|
|
)
|
|
}
|
|
|
|
override fun onViewThread(position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
super.viewThread(status.actionable.id, status.actionable.url)
|
|
}
|
|
|
|
override fun onViewTag(tag: String) {
|
|
if (viewModel.kind == TimelineViewModel.Kind.TAG && viewModel.tags.size == 1 &&
|
|
viewModel.tags.contains(tag)
|
|
) {
|
|
// If already viewing a tag page, then ignore any request to view that tag again.
|
|
return
|
|
}
|
|
super.viewTag(tag)
|
|
}
|
|
|
|
override fun onViewAccount(id: String) {
|
|
if ((
|
|
viewModel.kind == TimelineViewModel.Kind.USER ||
|
|
viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES
|
|
) &&
|
|
viewModel.id == id
|
|
) {
|
|
/* If already viewing an account page, then any requests to view that account page
|
|
* should be ignored. */
|
|
return
|
|
}
|
|
super.viewAccount(id)
|
|
}
|
|
|
|
private fun onPreferenceChanged(key: String) {
|
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
when (key) {
|
|
PrefKeys.FAB_HIDE -> {
|
|
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
|
}
|
|
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
|
|
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
|
|
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
|
if (enabled != oldMediaPreviewEnabled) {
|
|
adapter.mediaPreviewEnabled = enabled
|
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
|
}
|
|
}
|
|
PrefKeys.READING_ORDER -> {
|
|
readingOrder = ReadingOrder.from(
|
|
sharedPreferences.getString(PrefKeys.READING_ORDER, null)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun handleStatusComposeEvent(status: Status) {
|
|
when (kind) {
|
|
TimelineViewModel.Kind.HOME,
|
|
TimelineViewModel.Kind.PUBLIC_FEDERATED,
|
|
TimelineViewModel.Kind.PUBLIC_LOCAL -> adapter.refresh()
|
|
TimelineViewModel.Kind.USER,
|
|
TimelineViewModel.Kind.USER_WITH_REPLIES -> if (status.account.id == viewModel.id) {
|
|
adapter.refresh()
|
|
}
|
|
TimelineViewModel.Kind.TAG,
|
|
TimelineViewModel.Kind.FAVOURITES,
|
|
TimelineViewModel.Kind.LIST,
|
|
TimelineViewModel.Kind.BOOKMARKS,
|
|
TimelineViewModel.Kind.USER_PINNED -> return
|
|
}
|
|
}
|
|
|
|
public override fun removeItem(position: Int) {
|
|
val status = adapter.peek(position)?.asStatusOrNull() ?: return
|
|
viewModel.removeStatusWithId(status.id)
|
|
}
|
|
|
|
private fun actionButtonPresent(): Boolean {
|
|
return viewModel.kind != TimelineViewModel.Kind.TAG &&
|
|
viewModel.kind != TimelineViewModel.Kind.FAVOURITES &&
|
|
viewModel.kind != TimelineViewModel.Kind.BOOKMARKS &&
|
|
activity is ActionButtonActivity
|
|
}
|
|
|
|
private var talkBackWasEnabled = false
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
val a11yManager =
|
|
ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java)
|
|
|
|
val wasEnabled = talkBackWasEnabled
|
|
talkBackWasEnabled = a11yManager?.isEnabled == true
|
|
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
|
if (talkBackWasEnabled && !wasEnabled) {
|
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
|
}
|
|
startUpdateTimestamp()
|
|
}
|
|
|
|
/**
|
|
* Start to update adapter every minute to refresh timestamp
|
|
* If setting absoluteTimeView is false
|
|
* Auto dispose observable on pause
|
|
*/
|
|
private fun startUpdateTimestamp() {
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
|
if (!useAbsoluteTime) {
|
|
Observable.interval(0, 1, TimeUnit.MINUTES)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
|
.subscribe {
|
|
adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED))
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onReselect() {
|
|
if (isAdded) {
|
|
binding.recyclerView.layoutManager?.scrollToPosition(0)
|
|
binding.recyclerView.stopScroll()
|
|
}
|
|
}
|
|
|
|
override fun refreshContent() {
|
|
onRefresh()
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "TimelineF" // logging tag
|
|
private const val KIND_ARG = "kind"
|
|
private const val ID_ARG = "id"
|
|
private const val HASHTAGS_ARG = "hashtags"
|
|
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
|
|
|
|
fun newInstance(
|
|
kind: TimelineViewModel.Kind,
|
|
hashtagOrId: String? = null,
|
|
enableSwipeToRefresh: Boolean = true
|
|
): TimelineFragment {
|
|
val fragment = TimelineFragment()
|
|
val arguments = Bundle(3)
|
|
arguments.putString(KIND_ARG, kind.name)
|
|
arguments.putString(ID_ARG, hashtagOrId)
|
|
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
|
|
fragment.arguments = arguments
|
|
return fragment
|
|
}
|
|
|
|
@JvmStatic
|
|
fun newHashtagInstance(hashtags: List<String>): TimelineFragment {
|
|
val fragment = TimelineFragment()
|
|
val arguments = Bundle(3)
|
|
arguments.putString(KIND_ARG, TimelineViewModel.Kind.TAG.name)
|
|
arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags))
|
|
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
|
fragment.arguments = arguments
|
|
return fragment
|
|
}
|
|
}
|
|
}
|