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


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