logo

pleroma-fe

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

status.js (16833B)


  1. import ReplyButton from '../reply_button/reply_button.vue'
  2. import FavoriteButton from '../favorite_button/favorite_button.vue'
  3. import ReactButton from '../react_button/react_button.vue'
  4. import RetweetButton from '../retweet_button/retweet_button.vue'
  5. import ExtraButtons from '../extra_buttons/extra_buttons.vue'
  6. import PostStatusForm from '../post_status_form/post_status_form.vue'
  7. import UserAvatar from '../user_avatar/user_avatar.vue'
  8. import AvatarList from '../avatar_list/avatar_list.vue'
  9. import Timeago from '../timeago/timeago.vue'
  10. import StatusContent from '../status_content/status_content.vue'
  11. import RichContent from 'src/components/rich_content/rich_content.jsx'
  12. import StatusPopover from '../status_popover/status_popover.vue'
  13. import UserPopover from '../user_popover/user_popover.vue'
  14. import UserListPopover from '../user_list_popover/user_list_popover.vue'
  15. import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
  16. import UserLink from '../user_link/user_link.vue'
  17. import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
  18. import MentionLink from 'src/components/mention_link/mention_link.vue'
  19. import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
  20. import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
  21. import { muteWordHits } from '../../services/status_parser/status_parser.js'
  22. import { unescape, uniqBy } from 'lodash'
  23. import { library } from '@fortawesome/fontawesome-svg-core'
  24. import {
  25. faEnvelope,
  26. faLock,
  27. faLockOpen,
  28. faGlobe,
  29. faTimes,
  30. faRetweet,
  31. faReply,
  32. faPlusSquare,
  33. faSmileBeam,
  34. faEllipsisH,
  35. faStar,
  36. faEyeSlash,
  37. faEye,
  38. faThumbtack,
  39. faChevronUp,
  40. faChevronDown,
  41. faAngleDoubleRight,
  42. faPlay
  43. } from '@fortawesome/free-solid-svg-icons'
  44. library.add(
  45. faEnvelope,
  46. faGlobe,
  47. faLock,
  48. faLockOpen,
  49. faTimes,
  50. faRetweet,
  51. faReply,
  52. faPlusSquare,
  53. faStar,
  54. faSmileBeam,
  55. faEllipsisH,
  56. faEyeSlash,
  57. faEye,
  58. faThumbtack,
  59. faChevronUp,
  60. faChevronDown,
  61. faAngleDoubleRight,
  62. faPlay
  63. )
  64. const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
  65. const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
  66. const camelized = camelCase(name)
  67. const toggle = `controlledToggle${camelized}`
  68. const controlledName = `controlled${camelized}`
  69. const uncontrolledName = `uncontrolled${camelized}`
  70. res[name] = function () {
  71. return ((this.$data[toggle] !== undefined || this.$props[toggle] !== undefined) && this[toggle]) ? this[controlledName] : this[uncontrolledName]
  72. }
  73. return res
  74. }, {})
  75. const controlledOrUncontrolledToggle = (obj, name) => {
  76. const camelized = camelCase(name)
  77. const toggle = `controlledToggle${camelized}`
  78. const uncontrolledName = `uncontrolled${camelized}`
  79. if (obj[toggle]) {
  80. obj[toggle]()
  81. } else {
  82. obj[uncontrolledName] = !obj[uncontrolledName]
  83. }
  84. }
  85. const controlledOrUncontrolledSet = (obj, name, val) => {
  86. const camelized = camelCase(name)
  87. const set = `controlledSet${camelized}`
  88. const uncontrolledName = `uncontrolled${camelized}`
  89. if (obj[set]) {
  90. obj[set](val)
  91. } else {
  92. obj[uncontrolledName] = val
  93. }
  94. }
  95. const Status = {
  96. name: 'Status',
  97. components: {
  98. ReplyButton,
  99. FavoriteButton,
  100. ReactButton,
  101. RetweetButton,
  102. ExtraButtons,
  103. PostStatusForm,
  104. UserAvatar,
  105. AvatarList,
  106. Timeago,
  107. StatusPopover,
  108. UserListPopover,
  109. EmojiReactions,
  110. StatusContent,
  111. RichContent,
  112. MentionLink,
  113. MentionsLine,
  114. UserPopover,
  115. UserLink
  116. },
  117. props: [
  118. 'statusoid',
  119. 'expandable',
  120. 'inConversation',
  121. 'focused',
  122. 'highlight',
  123. 'compact',
  124. 'replies',
  125. 'isPreview',
  126. 'noHeading',
  127. 'inlineExpanded',
  128. 'showPinned',
  129. 'inProfile',
  130. 'profileUserId',
  131. 'inQuote',
  132. 'simpleTree',
  133. 'controlledThreadDisplayStatus',
  134. 'controlledToggleThreadDisplay',
  135. 'showOtherRepliesAsButton',
  136. 'controlledShowingTall',
  137. 'controlledToggleShowingTall',
  138. 'controlledExpandingSubject',
  139. 'controlledToggleExpandingSubject',
  140. 'controlledShowingLongSubject',
  141. 'controlledToggleShowingLongSubject',
  142. 'controlledReplying',
  143. 'controlledToggleReplying',
  144. 'controlledMediaPlaying',
  145. 'controlledSetMediaPlaying',
  146. 'dive'
  147. ],
  148. emits: ['interacted'],
  149. data () {
  150. return {
  151. uncontrolledReplying: false,
  152. unmuted: false,
  153. userExpanded: false,
  154. uncontrolledMediaPlaying: [],
  155. suspendable: true,
  156. error: null,
  157. headTailLinks: null,
  158. displayQuote: !this.inQuote
  159. }
  160. },
  161. computed: {
  162. ...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
  163. muteWords () {
  164. return this.mergedConfig.muteWords
  165. },
  166. showReasonMutedThread () {
  167. return (
  168. this.status.thread_muted ||
  169. (this.status.reblog && this.status.reblog.thread_muted)
  170. ) && !this.inConversation
  171. },
  172. repeaterClass () {
  173. const user = this.statusoid.user
  174. return highlightClass(user)
  175. },
  176. userClass () {
  177. const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
  178. return highlightClass(user)
  179. },
  180. deleted () {
  181. return this.statusoid.deleted
  182. },
  183. repeaterStyle () {
  184. const user = this.statusoid.user
  185. const highlight = this.mergedConfig.highlight
  186. return highlightStyle(highlight[user.screen_name])
  187. },
  188. userStyle () {
  189. if (this.noHeading) return
  190. const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
  191. const highlight = this.mergedConfig.highlight
  192. return highlightStyle(highlight[user.screen_name])
  193. },
  194. userProfileLink () {
  195. return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
  196. },
  197. replyProfileLink () {
  198. if (this.isReply) {
  199. const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
  200. // FIXME Why user not found sometimes???
  201. return user ? user.statusnet_profile_url : 'NOT_FOUND'
  202. }
  203. },
  204. retweet () { return !!this.statusoid.retweeted_status },
  205. retweeterUser () { return this.statusoid.user },
  206. retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
  207. retweeterHtml () { return this.statusoid.user.name },
  208. retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
  209. status () {
  210. if (this.retweet) {
  211. return this.statusoid.retweeted_status
  212. } else {
  213. return this.statusoid
  214. }
  215. },
  216. statusFromGlobalRepository () {
  217. // NOTE: Consider to replace status with statusFromGlobalRepository
  218. return this.$store.state.statuses.allStatusesObject[this.status.id]
  219. },
  220. loggedIn () {
  221. return !!this.currentUser
  222. },
  223. muteWordHits () {
  224. return muteWordHits(this.status, this.muteWords)
  225. },
  226. botStatus () {
  227. return this.status.user.actor_type === 'Service'
  228. },
  229. showActorTypeIndicator () {
  230. return !this.hideBotIndication
  231. },
  232. mentionsLine () {
  233. if (!this.headTailLinks) return []
  234. const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
  235. return this.status.attentions.filter(attn => {
  236. // no reply user
  237. return attn.id !== this.status.in_reply_to_user_id &&
  238. // no self-replies
  239. attn.statusnet_profile_url !== this.status.user.statusnet_profile_url &&
  240. // don't include if mentions is written
  241. !writtenSet.has(attn.statusnet_profile_url)
  242. }).map(attn => ({
  243. url: attn.statusnet_profile_url,
  244. content: attn.screen_name,
  245. userId: attn.id
  246. }))
  247. },
  248. hasMentionsLine () {
  249. return this.mentionsLine.length > 0
  250. },
  251. muted () {
  252. if (this.statusoid.user.id === this.currentUser.id) return false
  253. const reasonsToMute = this.userIsMuted ||
  254. // Thread is muted
  255. status.thread_muted ||
  256. // Wordfiltered
  257. this.muteWordHits.length > 0 ||
  258. // bot status
  259. (this.muteBotStatuses && this.botStatus && !this.compact)
  260. return !this.unmuted && !this.shouldNotMute && reasonsToMute
  261. },
  262. userIsMuted () {
  263. if (this.statusoid.user.id === this.currentUser.id) return false
  264. const { status } = this
  265. const { reblog } = status
  266. const relationship = this.$store.getters.relationship(status.user.id)
  267. const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
  268. return status.muted ||
  269. // Reprööt of a muted post according to BE
  270. (reblog && reblog.muted) ||
  271. // Muted user
  272. relationship.muting ||
  273. // Muted user of a reprööt
  274. (relationshipReblog && relationshipReblog.muting)
  275. },
  276. shouldNotMute () {
  277. const { status } = this
  278. const { reblog } = status
  279. return (
  280. (
  281. this.inProfile && (
  282. // Don't mute user's posts on user timeline (except reblogs)
  283. (!reblog && status.user.id === this.profileUserId) ||
  284. // Same as above but also allow self-reblogs
  285. (reblog && reblog.user.id === this.profileUserId)
  286. )
  287. ) ||
  288. // Don't mute statuses in muted conversation when said conversation is opened
  289. (this.inConversation && status.thread_muted)
  290. // No excuses if post has muted words
  291. ) && !this.muteWordHits.length > 0
  292. },
  293. hideMutedUsers () {
  294. return this.mergedConfig.hideMutedPosts
  295. },
  296. hideMutedThreads () {
  297. return this.mergedConfig.hideMutedThreads
  298. },
  299. hideFilteredStatuses () {
  300. return this.mergedConfig.hideFilteredStatuses
  301. },
  302. hideWordFilteredPosts () {
  303. return this.mergedConfig.hideWordFilteredPosts
  304. },
  305. hideStatus () {
  306. return (!this.shouldNotMute) && (
  307. (this.muted && this.hideFilteredStatuses) ||
  308. (this.userIsMuted && this.hideMutedUsers) ||
  309. (this.status.thread_muted && this.hideMutedThreads) ||
  310. (this.muteWordHits.length > 0 && this.hideWordFilteredPosts)
  311. )
  312. },
  313. isFocused () {
  314. // retweet or root of an expanded conversation
  315. if (this.focused) {
  316. return true
  317. } else if (!this.inConversation) {
  318. return false
  319. }
  320. // use conversation highlight only when in conversation
  321. return this.status.id === this.highlight
  322. },
  323. isReply () {
  324. return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
  325. },
  326. replyToName () {
  327. if (this.status.in_reply_to_screen_name) {
  328. return this.status.in_reply_to_screen_name
  329. } else {
  330. const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
  331. return user && user.screen_name_ui
  332. }
  333. },
  334. replySubject () {
  335. if (!this.status.summary) return ''
  336. const decodedSummary = unescape(this.status.summary)
  337. const behavior = this.mergedConfig.subjectLineBehavior
  338. const startsWithRe = decodedSummary.match(/^re[: ]/i)
  339. if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
  340. return decodedSummary
  341. } else if (behavior === 'email') {
  342. return 're: '.concat(decodedSummary)
  343. } else if (behavior === 'noop') {
  344. return ''
  345. }
  346. },
  347. combinedFavsAndRepeatsUsers () {
  348. // Use the status from the global status repository since favs and repeats are saved in it
  349. const combinedUsers = [].concat(
  350. this.statusFromGlobalRepository.favoritedBy,
  351. this.statusFromGlobalRepository.rebloggedBy
  352. )
  353. return uniqBy(combinedUsers, 'id')
  354. },
  355. tags () {
  356. // eslint-disable-next-line no-prototype-builtins
  357. return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
  358. },
  359. hidePostStats () {
  360. return this.mergedConfig.hidePostStats
  361. },
  362. muteBotStatuses () {
  363. return this.mergedConfig.muteBotStatuses
  364. },
  365. hideBotIndication () {
  366. return this.mergedConfig.hideBotIndication
  367. },
  368. currentUser () {
  369. return this.$store.state.users.currentUser
  370. },
  371. betterShadow () {
  372. return this.$store.state.interface.browserSupport.cssFilter
  373. },
  374. mergedConfig () {
  375. return this.$store.getters.mergedConfig
  376. },
  377. isSuspendable () {
  378. return !this.replying && this.mediaPlaying.length === 0
  379. },
  380. inThreadForest () {
  381. return !!this.controlledThreadDisplayStatus
  382. },
  383. threadShowing () {
  384. return this.controlledThreadDisplayStatus === 'showing'
  385. },
  386. visibilityLocalized () {
  387. return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
  388. },
  389. isEdited () {
  390. return this.status.edited_at !== null
  391. },
  392. editingAvailable () {
  393. return this.$store.state.instance.editingAvailable
  394. },
  395. hasVisibleQuote () {
  396. return this.status.quote_url && this.status.quote_visible
  397. },
  398. hasInvisibleQuote () {
  399. return this.status.quote_url && !this.status.quote_visible
  400. },
  401. quotedStatus () {
  402. return this.status.quote_id ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] : undefined
  403. },
  404. shouldDisplayQuote () {
  405. return this.quotedStatus && this.displayQuote
  406. },
  407. scrobblePresent () {
  408. return !this.mergedConfig.hideScrobbles && this.status.user.latestScrobble && this.status.user.latestScrobble.artist
  409. },
  410. scrobble () {
  411. return this.status.user.latestScrobble
  412. }
  413. },
  414. methods: {
  415. visibilityIcon (visibility) {
  416. switch (visibility) {
  417. case 'private':
  418. return 'lock'
  419. case 'unlisted':
  420. return 'lock-open'
  421. case 'direct':
  422. return 'envelope'
  423. default:
  424. return 'globe'
  425. }
  426. },
  427. showError (error) {
  428. this.error = error
  429. },
  430. clearError () {
  431. this.$emit('interacted')
  432. this.error = undefined
  433. },
  434. toggleReplying () {
  435. this.$emit('interacted')
  436. controlledOrUncontrolledToggle(this, 'replying')
  437. },
  438. gotoOriginal (id) {
  439. if (this.inConversation) {
  440. this.$emit('goto', id)
  441. }
  442. },
  443. toggleExpanded () {
  444. this.$emit('toggleExpanded')
  445. },
  446. toggleMute () {
  447. this.unmuted = !this.unmuted
  448. },
  449. toggleUserExpanded () {
  450. this.userExpanded = !this.userExpanded
  451. },
  452. generateUserProfileLink (id, name) {
  453. return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
  454. },
  455. addMediaPlaying (id) {
  456. controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
  457. },
  458. removeMediaPlaying (id) {
  459. controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
  460. },
  461. setHeadTailLinks (headTailLinks) {
  462. this.headTailLinks = headTailLinks
  463. },
  464. toggleThreadDisplay () {
  465. this.controlledToggleThreadDisplay()
  466. },
  467. scrollIfHighlighted (highlightId) {
  468. const id = highlightId
  469. if (this.status.id === id) {
  470. const rect = this.$el.getBoundingClientRect()
  471. if (rect.top < 100) {
  472. // Post is above screen, match its top to screen top
  473. window.scrollBy(0, rect.top - 100)
  474. } else if (rect.height >= (window.innerHeight - 50)) {
  475. // Post we want to see is taller than screen so match its top to screen top
  476. window.scrollBy(0, rect.top - 100)
  477. } else if (rect.bottom > window.innerHeight - 50) {
  478. // Post is below screen, match its bottom to screen bottom
  479. window.scrollBy(0, rect.bottom - window.innerHeight + 50)
  480. }
  481. }
  482. },
  483. toggleDisplayQuote () {
  484. if (this.shouldDisplayQuote) {
  485. this.displayQuote = false
  486. } else if (!this.quotedStatus) {
  487. this.$store.dispatch('fetchStatus', this.status.quote_id)
  488. .then(() => {
  489. this.displayQuote = true
  490. })
  491. } else {
  492. this.displayQuote = true
  493. }
  494. }
  495. },
  496. watch: {
  497. highlight: function (id) {
  498. this.scrollIfHighlighted(id)
  499. },
  500. 'status.repeat_num': function (num) {
  501. // refetch repeats when repeat_num is changed in any way
  502. if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {
  503. this.$store.dispatch('fetchRepeats', this.status.id)
  504. }
  505. },
  506. 'status.fave_num': function (num) {
  507. // refetch favs when fave_num is changed in any way
  508. if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
  509. this.$store.dispatch('fetchFavs', this.status.id)
  510. }
  511. },
  512. isSuspendable: function (val) {
  513. this.suspendable = val
  514. }
  515. }
  516. }
  517. export default Status