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 (9795B)


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