logo

pleroma-fe

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

status.js (18841B)


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