logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

timeline.js (9981B)


  1. import Status from '../status/status.vue'
  2. import { mapState } from 'pinia'
  3. import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
  4. import Conversation from '../conversation/conversation.vue'
  5. import TimelineMenu from '../timeline_menu/timeline_menu.vue'
  6. import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
  7. import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
  8. import { debounce, throttle, keyBy } from 'lodash'
  9. import { library } from '@fortawesome/fontawesome-svg-core'
  10. import { faCircleNotch, faCirclePlus, faCog, faMinus, faArrowUp, faCheck } from '@fortawesome/free-solid-svg-icons'
  11. import { useInterfaceStore } from 'src/stores/interface'
  12. library.add(
  13. faCircleNotch,
  14. faCog,
  15. faMinus,
  16. faArrowUp,
  17. faCirclePlus,
  18. faCheck
  19. )
  20. const Timeline = {
  21. props: [
  22. 'timeline',
  23. 'timelineName',
  24. 'title',
  25. 'userId',
  26. 'listId',
  27. 'statusId',
  28. 'bookmarkFolderId',
  29. 'tag',
  30. 'embedded',
  31. 'count',
  32. 'pinnedStatusIds',
  33. 'inProfile',
  34. 'footerSlipgate' // reference to an element where we should put our footer
  35. ],
  36. data () {
  37. return {
  38. showScrollTop: false,
  39. paused: false,
  40. unfocused: false,
  41. bottomedOut: false,
  42. virtualScrollIndex: 0,
  43. blockingClicks: false
  44. }
  45. },
  46. components: {
  47. Status,
  48. Conversation,
  49. TimelineMenu,
  50. QuickFilterSettings,
  51. QuickViewSettings
  52. },
  53. computed: {
  54. filteredVisibleStatuses () {
  55. return this.timeline.visibleStatuses.filter(status => this.timelineName !== 'user' || (status.id >= this.timeline.minId && status.id <= this.timeline.maxId))
  56. },
  57. filteredPinnedStatusIds () {
  58. return (this.pinnedStatusIds || []).filter(statusId => this.timeline.statusesObject[statusId])
  59. },
  60. newStatusCount () {
  61. return this.timeline.newStatusCount
  62. },
  63. showLoadButton () {
  64. return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
  65. },
  66. loadButtonString () {
  67. if (this.timeline.flushMarker !== 0) {
  68. return this.$t('timeline.reload')
  69. } else {
  70. return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
  71. }
  72. },
  73. mobileLoadButtonString () {
  74. if (this.timeline.flushMarker !== 0) {
  75. return '+'
  76. } else {
  77. return this.newStatusCount > 99 ? '∞' : this.newStatusCount
  78. }
  79. },
  80. classes () {
  81. let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-embedded']
  82. if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
  83. return {
  84. root: rootClasses,
  85. header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body']),
  86. body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : ['panel-body']),
  87. footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : ['panel-body'])
  88. }
  89. },
  90. // id map of statuses which need to be hidden in the main list due to pinning logic
  91. pinnedStatusIdsObject () {
  92. return keyBy(this.pinnedStatusIds)
  93. },
  94. statusesToDisplay () {
  95. const amount = this.timeline.visibleStatuses.length
  96. const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
  97. const nonPinnedIndex = this.virtualScrollIndex - this.filteredPinnedStatusIds.length
  98. const min = Math.max(0, nonPinnedIndex - statusesPerSide)
  99. const max = Math.min(amount, nonPinnedIndex + statusesPerSide)
  100. return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
  101. },
  102. virtualScrollingEnabled () {
  103. return this.$store.getters.mergedConfig.virtualScrolling
  104. },
  105. ...mapState(useInterfaceStore, {
  106. mobileLayout: store => store.layoutType === 'mobile'
  107. })
  108. },
  109. created () {
  110. const store = this.$store
  111. const credentials = store.state.users.currentUser.credentials
  112. const showImmediately = this.timeline.visibleStatuses.length === 0
  113. window.addEventListener('scroll', this.handleScroll)
  114. if (store.state.api.fetchers[this.timelineName]) { return false }
  115. timelineFetcher.fetchAndUpdate({
  116. store,
  117. credentials,
  118. timeline: this.timelineName,
  119. showImmediately,
  120. userId: this.userId,
  121. listId: this.listId,
  122. statusId: this.statusId,
  123. bookmarkFolderId: this.bookmarkFolderId,
  124. tag: this.tag
  125. })
  126. },
  127. mounted () {
  128. if (typeof document.hidden !== 'undefined') {
  129. document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
  130. this.unfocused = document.hidden
  131. }
  132. window.addEventListener('keydown', this.handleShortKey)
  133. setTimeout(this.determineVisibleStatuses, 250)
  134. },
  135. unmounted () {
  136. window.removeEventListener('scroll', this.handleScroll)
  137. window.removeEventListener('keydown', this.handleShortKey)
  138. if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
  139. this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
  140. },
  141. methods: {
  142. scrollToTop () {
  143. window.scrollTo({ top: this.$el.offsetTop })
  144. },
  145. stopBlockingClicks: debounce(function () {
  146. this.blockingClicks = false
  147. }, 1000),
  148. blockClicksTemporarily () {
  149. if (!this.blockingClicks) {
  150. this.blockingClicks = true
  151. }
  152. this.stopBlockingClicks()
  153. },
  154. handleShortKey (e) {
  155. // Ignore when input fields are focused
  156. if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
  157. if (e.key === '.') this.showNewStatuses()
  158. },
  159. showNewStatuses () {
  160. if (this.timeline.flushMarker !== 0) {
  161. this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
  162. this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
  163. if (this.timelineName === 'user') {
  164. this.$store.dispatch('fetchPinnedStatuses', this.userId)
  165. }
  166. this.fetchOlderStatuses()
  167. } else {
  168. this.blockClicksTemporarily()
  169. this.$store.commit('showNewStatuses', { timeline: this.timelineName })
  170. this.paused = false
  171. }
  172. window.scrollTo({ top: 0 })
  173. },
  174. fetchOlderStatuses: throttle(function () {
  175. const store = this.$store
  176. const credentials = store.state.users.currentUser.credentials
  177. store.commit('setLoading', { timeline: this.timelineName, value: true })
  178. timelineFetcher.fetchAndUpdate({
  179. store,
  180. credentials,
  181. timeline: this.timelineName,
  182. older: true,
  183. showImmediately: true,
  184. userId: this.userId,
  185. listId: this.listId,
  186. statusId: this.statusId,
  187. bookmarkFolderId: this.bookmarkFolderId,
  188. tag: this.tag
  189. }).then(({ statuses }) => {
  190. if (statuses && statuses.length === 0) {
  191. this.bottomedOut = true
  192. }
  193. }).finally(() =>
  194. store.commit('setLoading', { timeline: this.timelineName, value: false })
  195. )
  196. }, 1000, this),
  197. determineVisibleStatuses () {
  198. if (!this.$refs.timeline) return
  199. if (!this.virtualScrollingEnabled) return
  200. const statuses = this.$refs.timeline.children
  201. const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1))
  202. if (statuses.length === 0) return
  203. const height = Math.max(document.body.offsetHeight, window.pageYOffset)
  204. const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
  205. // Start from approximating the index of some visible status by using the
  206. // the center of the screen on the timeline.
  207. let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
  208. let err = statuses[approxIndex].getBoundingClientRect().y
  209. // if we have a previous scroll index that can be used, test if it's
  210. // closer than the previous approximation, use it if so
  211. const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y
  212. if (Math.abs(err) > virtualScrollIndexY) {
  213. approxIndex = cappedScrollIndex
  214. err = virtualScrollIndexY
  215. }
  216. // if the status is too far from viewport, check the next/previous ones if
  217. // they happen to be better
  218. while (err < -20 && approxIndex < statuses.length - 1) {
  219. err += statuses[approxIndex].offsetHeight
  220. approxIndex++
  221. }
  222. while (err > window.innerHeight + 100 && approxIndex > 0) {
  223. approxIndex--
  224. err -= statuses[approxIndex].offsetHeight
  225. }
  226. // this status is now the center point for virtual scrolling and visible
  227. // statuses will be nearby statuses before and after it
  228. this.virtualScrollIndex = approxIndex
  229. },
  230. scrollLoad () {
  231. const bodyBRect = document.body.getBoundingClientRect()
  232. const height = Math.max(bodyBRect.height, -(bodyBRect.y))
  233. if (this.timeline.loading === false &&
  234. this.$el.offsetHeight > 0 &&
  235. (window.innerHeight + window.pageYOffset) >= (height - 750)) {
  236. this.fetchOlderStatuses()
  237. }
  238. },
  239. handleScroll: throttle(function (e) {
  240. this.showScrollTop = this.$el.offsetTop < window.scrollY
  241. this.determineVisibleStatuses()
  242. this.scrollLoad(e)
  243. }, 200),
  244. handleVisibilityChange () {
  245. this.unfocused = document.hidden
  246. }
  247. },
  248. watch: {
  249. newStatusCount (count) {
  250. if (!this.$store.getters.mergedConfig.streaming) {
  251. return
  252. }
  253. if (count > 0) {
  254. // only 'stream' them when you're scrolled to the top
  255. const doc = document.documentElement
  256. const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
  257. if (top < 15 &&
  258. !this.paused &&
  259. !(this.unfocused && this.$store.getters.mergedConfig.pauseOnUnfocused)
  260. ) {
  261. this.showNewStatuses()
  262. } else {
  263. this.paused = true
  264. }
  265. }
  266. }
  267. }
  268. }
  269. export default Timeline