TimelineFragment.kt 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. /* Copyright 2021 Tusky Contributors
  2. *
  3. * This file is a part of Tusky.
  4. *
  5. * This program is free software; you can redistribute it and/or modify it under the terms of the
  6. * GNU General Public License as published by the Free Software Foundation; either version 3 of the
  7. * License, or (at your option) any later version.
  8. *
  9. * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
  10. * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
  11. * Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along with Tusky; if not,
  14. * see <http://www.gnu.org/licenses>. */
  15. package com.keylesspalace.tusky.components.timeline
  16. import android.os.Bundle
  17. import android.util.Log
  18. import android.view.LayoutInflater
  19. import android.view.Menu
  20. import android.view.MenuInflater
  21. import android.view.MenuItem
  22. import android.view.View
  23. import android.view.ViewGroup
  24. import android.view.accessibility.AccessibilityManager
  25. import androidx.core.content.ContextCompat
  26. import androidx.core.view.MenuProvider
  27. import androidx.lifecycle.Lifecycle
  28. import androidx.lifecycle.ViewModelProvider
  29. import androidx.lifecycle.lifecycleScope
  30. import androidx.paging.LoadState
  31. import androidx.preference.PreferenceManager
  32. import androidx.recyclerview.widget.DividerItemDecoration
  33. import androidx.recyclerview.widget.LinearLayoutManager
  34. import androidx.recyclerview.widget.RecyclerView
  35. import androidx.recyclerview.widget.SimpleItemAnimator
  36. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
  37. import at.connyduck.sparkbutton.helpers.Utils
  38. import autodispose2.androidx.lifecycle.autoDispose
  39. import com.google.android.material.color.MaterialColors
  40. import com.keylesspalace.tusky.BaseActivity
  41. import com.keylesspalace.tusky.R
  42. import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
  43. import com.keylesspalace.tusky.appstore.EventHub
  44. import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
  45. import com.keylesspalace.tusky.appstore.StatusComposedEvent
  46. import com.keylesspalace.tusky.components.accountlist.AccountListActivity
  47. import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent
  48. import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
  49. import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
  50. import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
  51. import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
  52. import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
  53. import com.keylesspalace.tusky.di.Injectable
  54. import com.keylesspalace.tusky.di.ViewModelFactory
  55. import com.keylesspalace.tusky.entity.Status
  56. import com.keylesspalace.tusky.fragment.SFragment
  57. import com.keylesspalace.tusky.interfaces.ActionButtonActivity
  58. import com.keylesspalace.tusky.interfaces.RefreshableFragment
  59. import com.keylesspalace.tusky.interfaces.ReselectableFragment
  60. import com.keylesspalace.tusky.interfaces.StatusActionListener
  61. import com.keylesspalace.tusky.settings.PrefKeys
  62. import com.keylesspalace.tusky.util.CardViewMode
  63. import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
  64. import com.keylesspalace.tusky.util.StatusDisplayOptions
  65. import com.keylesspalace.tusky.util.hide
  66. import com.keylesspalace.tusky.util.show
  67. import com.keylesspalace.tusky.util.unsafeLazy
  68. import com.keylesspalace.tusky.util.viewBinding
  69. import com.keylesspalace.tusky.viewdata.AttachmentViewData
  70. import com.keylesspalace.tusky.viewdata.StatusViewData
  71. import com.mikepenz.iconics.IconicsDrawable
  72. import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
  73. import com.mikepenz.iconics.utils.colorInt
  74. import com.mikepenz.iconics.utils.sizeDp
  75. import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
  76. import io.reactivex.rxjava3.core.Observable
  77. import kotlinx.coroutines.flow.collectLatest
  78. import kotlinx.coroutines.launch
  79. import java.io.IOException
  80. import java.util.concurrent.TimeUnit
  81. import javax.inject.Inject
  82. class TimelineFragment :
  83. SFragment(),
  84. OnRefreshListener,
  85. StatusActionListener,
  86. Injectable,
  87. ReselectableFragment,
  88. RefreshableFragment,
  89. MenuProvider {
  90. @Inject
  91. lateinit var viewModelFactory: ViewModelFactory
  92. @Inject
  93. lateinit var eventHub: EventHub
  94. private val viewModel: TimelineViewModel by unsafeLazy {
  95. if (kind == TimelineViewModel.Kind.HOME) {
  96. ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
  97. } else {
  98. ViewModelProvider(this, viewModelFactory)[NetworkTimelineViewModel::class.java]
  99. }
  100. }
  101. private val binding by viewBinding(FragmentTimelineBinding::bind)
  102. private lateinit var kind: TimelineViewModel.Kind
  103. private lateinit var adapter: TimelinePagingAdapter
  104. private var isSwipeToRefreshEnabled = true
  105. private var hideFab = false
  106. /**
  107. * Adapter position of the placeholder that was most recently clicked to "Load more". If null
  108. * then there is no active "Load more" operation
  109. */
  110. private var loadMorePosition: Int? = null
  111. /** ID of the status immediately below the most recent "Load more" placeholder click */
  112. // The Paging library assumes that the user will be scrolling down a list of items,
  113. // and if new items are loaded but not visible then it's reasonable to scroll to the top
  114. // of the inserted items. It does not seem to be possible to disable that behaviour.
  115. //
  116. // That behaviour should depend on the user's preferred reading order. If they prefer to
  117. // read oldest first then the list should be scrolled to the bottom of the freshly
  118. // inserted statuses.
  119. //
  120. // To do this:
  121. //
  122. // 1. When "Load more" is clicked (onLoadMore()):
  123. // a. Remember the adapter position of the "Load more" item in loadMorePosition
  124. // b. Remember the ID of the status immediately below the "Load more" item in
  125. // statusIdBelowLoadMore
  126. // 2. After the new items have been inserted, search the adapter for the position of the
  127. // status with id == statusIdBelowLoadMore.
  128. // 3. If this position is still visible on screen then do nothing, otherwise, scroll the view
  129. // so that the status is visible.
  130. //
  131. // The user can then scroll up to read the new statuses.
  132. private var statusIdBelowLoadMore: String? = null
  133. /** The user's preferred reading order */
  134. private lateinit var readingOrder: ReadingOrder
  135. override fun onCreate(savedInstanceState: Bundle?) {
  136. super.onCreate(savedInstanceState)
  137. val arguments = requireArguments()
  138. kind = TimelineViewModel.Kind.valueOf(arguments.getString(KIND_ARG)!!)
  139. val id: String? = if (kind == TimelineViewModel.Kind.USER ||
  140. kind == TimelineViewModel.Kind.USER_PINNED ||
  141. kind == TimelineViewModel.Kind.USER_WITH_REPLIES ||
  142. kind == TimelineViewModel.Kind.LIST
  143. ) {
  144. arguments.getString(ID_ARG)!!
  145. } else {
  146. null
  147. }
  148. val tags = if (kind == TimelineViewModel.Kind.TAG) {
  149. arguments.getStringArrayList(HASHTAGS_ARG)!!
  150. } else {
  151. listOf()
  152. }
  153. viewModel.init(
  154. kind,
  155. id,
  156. tags,
  157. )
  158. isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
  159. val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
  160. readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null))
  161. val statusDisplayOptions = StatusDisplayOptions(
  162. animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
  163. mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
  164. useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
  165. showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
  166. useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
  167. cardViewMode = if (preferences.getBoolean(
  168. PrefKeys.SHOW_CARDS_IN_TIMELINES,
  169. false
  170. )
  171. ) CardViewMode.INDENTED else CardViewMode.NONE,
  172. confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true),
  173. confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false),
  174. hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
  175. animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
  176. showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia,
  177. openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
  178. )
  179. adapter = TimelinePagingAdapter(
  180. statusDisplayOptions,
  181. this
  182. )
  183. }
  184. override fun onCreateView(
  185. inflater: LayoutInflater,
  186. container: ViewGroup?,
  187. savedInstanceState: Bundle?
  188. ): View? {
  189. return inflater.inflate(R.layout.fragment_timeline, container, false)
  190. }
  191. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  192. requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
  193. setupSwipeRefreshLayout()
  194. setupRecyclerView()
  195. adapter.addLoadStateListener { loadState ->
  196. if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
  197. binding.swipeRefreshLayout.isRefreshing = false
  198. }
  199. binding.statusView.hide()
  200. binding.progressBar.hide()
  201. if (adapter.itemCount == 0) {
  202. when (loadState.refresh) {
  203. is LoadState.NotLoading -> {
  204. if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
  205. binding.statusView.show()
  206. binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
  207. }
  208. }
  209. is LoadState.Error -> {
  210. binding.statusView.show()
  211. if ((loadState.refresh as LoadState.Error).error is IOException) {
  212. binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network)
  213. } else {
  214. binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic)
  215. }
  216. }
  217. is LoadState.Loading -> {
  218. binding.progressBar.show()
  219. }
  220. }
  221. }
  222. }
  223. adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
  224. override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
  225. if (positionStart == 0 && adapter.itemCount != itemCount) {
  226. binding.recyclerView.post {
  227. if (getView() != null) {
  228. if (isSwipeToRefreshEnabled) {
  229. binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
  230. } else binding.recyclerView.scrollToPosition(0)
  231. }
  232. }
  233. }
  234. if (readingOrder == ReadingOrder.OLDEST_FIRST) {
  235. updateReadingPositionForOldestFirst()
  236. }
  237. }
  238. })
  239. viewLifecycleOwner.lifecycleScope.launch {
  240. viewModel.statuses.collectLatest { pagingData ->
  241. adapter.submitData(pagingData)
  242. }
  243. }
  244. if (actionButtonPresent()) {
  245. val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
  246. hideFab = preferences.getBoolean("fabHide", false)
  247. binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  248. override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
  249. val composeButton = (activity as ActionButtonActivity).actionButton
  250. if (composeButton != null) {
  251. if (hideFab) {
  252. if (dy > 0 && composeButton.isShown) {
  253. composeButton.hide() // hides the button if we're scrolling down
  254. } else if (dy < 0 && !composeButton.isShown) {
  255. composeButton.show() // shows it if we are scrolling up
  256. }
  257. } else if (!composeButton.isShown) {
  258. composeButton.show()
  259. }
  260. }
  261. }
  262. })
  263. }
  264. eventHub.events
  265. .observeOn(AndroidSchedulers.mainThread())
  266. .autoDispose(this, Lifecycle.Event.ON_DESTROY)
  267. .subscribe { event ->
  268. when (event) {
  269. is PreferenceChangedEvent -> {
  270. onPreferenceChanged(event.preferenceKey)
  271. }
  272. is StatusComposedEvent -> {
  273. val status = event.status
  274. handleStatusComposeEvent(status)
  275. }
  276. }
  277. }
  278. }
  279. override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
  280. if (isSwipeToRefreshEnabled) {
  281. menuInflater.inflate(R.menu.fragment_timeline, menu)
  282. menu.findItem(R.id.action_refresh)?.apply {
  283. icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
  284. sizeDp = 20
  285. colorInt =
  286. MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
  287. }
  288. }
  289. }
  290. }
  291. override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
  292. return when (menuItem.itemId) {
  293. R.id.action_refresh -> {
  294. if (isSwipeToRefreshEnabled) {
  295. binding.swipeRefreshLayout.isRefreshing = true
  296. refreshContent()
  297. true
  298. } else {
  299. false
  300. }
  301. }
  302. else -> false
  303. }
  304. }
  305. /**
  306. * Set the correct reading position in the timeline after the user clicked "Load more",
  307. * assuming the reading position should be below the freshly-loaded statuses.
  308. */
  309. // Note: The positionStart parameter to onItemRangeInserted() does not always
  310. // match the adapter position where data was inserted (which is why loadMorePosition
  311. // is tracked manually, see this bug report for another example:
  312. // https://github.com/android/architecture-components-samples/issues/726).
  313. private fun updateReadingPositionForOldestFirst() {
  314. var position = loadMorePosition ?: return
  315. val statusIdBelowLoadMore = statusIdBelowLoadMore ?: return
  316. var status: StatusViewData?
  317. while (adapter.peek(position).let { status = it; it != null }) {
  318. if (status?.id == statusIdBelowLoadMore) {
  319. val lastVisiblePosition =
  320. (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
  321. if (position > lastVisiblePosition) {
  322. binding.recyclerView.scrollToPosition(position)
  323. }
  324. break
  325. }
  326. position++
  327. }
  328. loadMorePosition = null
  329. }
  330. private fun setupSwipeRefreshLayout() {
  331. binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
  332. binding.swipeRefreshLayout.setOnRefreshListener(this)
  333. binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
  334. }
  335. private fun setupRecyclerView() {
  336. binding.recyclerView.setAccessibilityDelegateCompat(
  337. ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
  338. if (pos in 0 until adapter.itemCount) {
  339. adapter.peek(pos)
  340. } else {
  341. null
  342. }
  343. }
  344. )
  345. binding.recyclerView.setHasFixedSize(true)
  346. binding.recyclerView.layoutManager = LinearLayoutManager(context)
  347. val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
  348. binding.recyclerView.addItemDecoration(divider)
  349. // CWs are expanded without animation, buttons animate itself, we don't need it basically
  350. (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
  351. binding.recyclerView.adapter = adapter
  352. }
  353. override fun onRefresh() {
  354. binding.statusView.hide()
  355. adapter.refresh()
  356. }
  357. override fun onReply(position: Int) {
  358. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  359. super.reply(status.status)
  360. }
  361. override fun onReblog(reblog: Boolean, position: Int) {
  362. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  363. viewModel.reblog(reblog, status)
  364. }
  365. override fun onFavourite(favourite: Boolean, position: Int) {
  366. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  367. viewModel.favorite(favourite, status)
  368. }
  369. override fun onBookmark(bookmark: Boolean, position: Int) {
  370. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  371. viewModel.bookmark(bookmark, status)
  372. }
  373. override fun onVoteInPoll(position: Int, choices: List<Int>) {
  374. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  375. viewModel.voteInPoll(choices, status)
  376. }
  377. override fun onMore(view: View, position: Int) {
  378. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  379. super.more(status.status, view, position)
  380. }
  381. override fun onOpenReblog(position: Int) {
  382. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  383. super.openReblog(status.status)
  384. }
  385. override fun onExpandedChange(expanded: Boolean, position: Int) {
  386. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  387. viewModel.changeExpanded(expanded, status)
  388. }
  389. override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
  390. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  391. viewModel.changeContentShowing(isShowing, status)
  392. }
  393. override fun onShowReblogs(position: Int) {
  394. val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
  395. val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
  396. (activity as BaseActivity).startActivityWithSlideInAnimation(intent)
  397. }
  398. override fun onShowFavs(position: Int) {
  399. val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
  400. val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
  401. (activity as BaseActivity).startActivityWithSlideInAnimation(intent)
  402. }
  403. override fun onLoadMore(position: Int) {
  404. val placeholder = adapter.peek(position)?.asPlaceholderOrNull() ?: return
  405. loadMorePosition = position
  406. statusIdBelowLoadMore = adapter.peek(position + 1)?.id
  407. viewModel.loadMore(placeholder.id)
  408. }
  409. override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
  410. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  411. viewModel.changeContentCollapsed(isCollapsed, status)
  412. }
  413. override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
  414. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  415. super.viewMedia(
  416. attachmentIndex,
  417. AttachmentViewData.list(status.actionable),
  418. view
  419. )
  420. }
  421. override fun onViewThread(position: Int) {
  422. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  423. super.viewThread(status.actionable.id, status.actionable.url)
  424. }
  425. override fun onViewTag(tag: String) {
  426. if (viewModel.kind == TimelineViewModel.Kind.TAG && viewModel.tags.size == 1 &&
  427. viewModel.tags.contains(tag)
  428. ) {
  429. // If already viewing a tag page, then ignore any request to view that tag again.
  430. return
  431. }
  432. super.viewTag(tag)
  433. }
  434. override fun onViewAccount(id: String) {
  435. if ((
  436. viewModel.kind == TimelineViewModel.Kind.USER ||
  437. viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES
  438. ) &&
  439. viewModel.id == id
  440. ) {
  441. /* If already viewing an account page, then any requests to view that account page
  442. * should be ignored. */
  443. return
  444. }
  445. super.viewAccount(id)
  446. }
  447. private fun onPreferenceChanged(key: String) {
  448. val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
  449. when (key) {
  450. PrefKeys.FAB_HIDE -> {
  451. hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
  452. }
  453. PrefKeys.MEDIA_PREVIEW_ENABLED -> {
  454. val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
  455. val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
  456. if (enabled != oldMediaPreviewEnabled) {
  457. adapter.mediaPreviewEnabled = enabled
  458. adapter.notifyItemRangeChanged(0, adapter.itemCount)
  459. }
  460. }
  461. PrefKeys.READING_ORDER -> {
  462. readingOrder = ReadingOrder.from(
  463. sharedPreferences.getString(PrefKeys.READING_ORDER, null)
  464. )
  465. }
  466. }
  467. }
  468. private fun handleStatusComposeEvent(status: Status) {
  469. when (kind) {
  470. TimelineViewModel.Kind.HOME,
  471. TimelineViewModel.Kind.PUBLIC_FEDERATED,
  472. TimelineViewModel.Kind.PUBLIC_LOCAL -> adapter.refresh()
  473. TimelineViewModel.Kind.USER,
  474. TimelineViewModel.Kind.USER_WITH_REPLIES -> if (status.account.id == viewModel.id) {
  475. adapter.refresh()
  476. }
  477. TimelineViewModel.Kind.TAG,
  478. TimelineViewModel.Kind.FAVOURITES,
  479. TimelineViewModel.Kind.LIST,
  480. TimelineViewModel.Kind.BOOKMARKS,
  481. TimelineViewModel.Kind.USER_PINNED -> return
  482. }
  483. }
  484. public override fun removeItem(position: Int) {
  485. val status = adapter.peek(position)?.asStatusOrNull() ?: return
  486. viewModel.removeStatusWithId(status.id)
  487. }
  488. private fun actionButtonPresent(): Boolean {
  489. return viewModel.kind != TimelineViewModel.Kind.TAG &&
  490. viewModel.kind != TimelineViewModel.Kind.FAVOURITES &&
  491. viewModel.kind != TimelineViewModel.Kind.BOOKMARKS &&
  492. activity is ActionButtonActivity
  493. }
  494. private var talkBackWasEnabled = false
  495. override fun onResume() {
  496. super.onResume()
  497. val a11yManager =
  498. ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java)
  499. val wasEnabled = talkBackWasEnabled
  500. talkBackWasEnabled = a11yManager?.isEnabled == true
  501. Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
  502. if (talkBackWasEnabled && !wasEnabled) {
  503. adapter.notifyItemRangeChanged(0, adapter.itemCount)
  504. }
  505. startUpdateTimestamp()
  506. }
  507. /**
  508. * Start to update adapter every minute to refresh timestamp
  509. * If setting absoluteTimeView is false
  510. * Auto dispose observable on pause
  511. */
  512. private fun startUpdateTimestamp() {
  513. val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
  514. val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
  515. if (!useAbsoluteTime) {
  516. Observable.interval(0, 1, TimeUnit.MINUTES)
  517. .observeOn(AndroidSchedulers.mainThread())
  518. .autoDispose(this, Lifecycle.Event.ON_PAUSE)
  519. .subscribe {
  520. adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED))
  521. }
  522. }
  523. }
  524. override fun onReselect() {
  525. if (isAdded) {
  526. binding.recyclerView.layoutManager?.scrollToPosition(0)
  527. binding.recyclerView.stopScroll()
  528. }
  529. }
  530. override fun refreshContent() {
  531. onRefresh()
  532. }
  533. companion object {
  534. private const val TAG = "TimelineF" // logging tag
  535. private const val KIND_ARG = "kind"
  536. private const val ID_ARG = "id"
  537. private const val HASHTAGS_ARG = "hashtags"
  538. private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
  539. fun newInstance(
  540. kind: TimelineViewModel.Kind,
  541. hashtagOrId: String? = null,
  542. enableSwipeToRefresh: Boolean = true
  543. ): TimelineFragment {
  544. val fragment = TimelineFragment()
  545. val arguments = Bundle(3)
  546. arguments.putString(KIND_ARG, kind.name)
  547. arguments.putString(ID_ARG, hashtagOrId)
  548. arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
  549. fragment.arguments = arguments
  550. return fragment
  551. }
  552. @JvmStatic
  553. fun newHashtagInstance(hashtags: List<String>): TimelineFragment {
  554. val fragment = TimelineFragment()
  555. val arguments = Bundle(3)
  556. arguments.putString(KIND_ARG, TimelineViewModel.Kind.TAG.name)
  557. arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags))
  558. arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
  559. fragment.arguments = arguments
  560. return fragment
  561. }
  562. }
  563. }