logo

pleroma-fe

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

timeline.js (9715B)


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