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


  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. 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. return this.$t(
  275. 'status.multi_reason_mute',
  276. {
  277. main: mainReason(),
  278. numReasonsMore: this.muteReasons.length - 1
  279. },
  280. this.muteReasons.length - 1
  281. )
  282. },
  283. muted () {
  284. if (this.statusoid.user.id === this.currentUser.id) return false
  285. return !this.unmuted && !this.shouldNotMute && this.muteReasons.length > 0
  286. },
  287. userIsMuted () {
  288. if (this.statusoid.user.id === this.currentUser.id) return false
  289. const { status } = this
  290. const { reblog } = status
  291. const relationship = this.$store.getters.relationship(status.user.id)
  292. const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
  293. return status.muted ||
  294. // Reprööt of a muted post according to BE
  295. (reblog && reblog.muted) ||
  296. // Muted user
  297. relationship.muting ||
  298. // Muted user of a reprööt
  299. (relationshipReblog && relationshipReblog.muting)
  300. },
  301. shouldNotMute () {
  302. const { status } = this
  303. const { reblog } = status
  304. return (
  305. (
  306. this.inProfile && (
  307. // Don't mute user's posts on user timeline (except reblogs)
  308. (!reblog && status.user.id === this.profileUserId) ||
  309. // Same as above but also allow self-reblogs
  310. (reblog && reblog.user.id === this.profileUserId)
  311. )
  312. ) ||
  313. // Don't mute statuses in muted conversation when said conversation is opened
  314. (this.inConversation && status.thread_muted)
  315. // No excuses if post has muted words
  316. ) && !this.muteWordHits.length > 0
  317. },
  318. hideMutedUsers () {
  319. return this.mergedConfig.hideMutedPosts
  320. },
  321. hideMutedThreads () {
  322. return this.mergedConfig.hideMutedThreads
  323. },
  324. hideFilteredStatuses () {
  325. return this.mergedConfig.hideFilteredStatuses
  326. },
  327. hideWordFilteredPosts () {
  328. return this.mergedConfig.hideWordFilteredPosts
  329. },
  330. hideStatus () {
  331. return (!this.shouldNotMute) && (
  332. (this.muted && this.hideFilteredStatuses) ||
  333. (this.userIsMuted && this.hideMutedUsers) ||
  334. (this.status.thread_muted && this.hideMutedThreads) ||
  335. (this.muteWordHits.length > 0 && this.hideWordFilteredPosts)
  336. )
  337. },
  338. isFocused () {
  339. // retweet or root of an expanded conversation
  340. if (this.focused) {
  341. return true
  342. } else if (!this.inConversation) {
  343. return false
  344. }
  345. // use conversation highlight only when in conversation
  346. return this.status.id === this.highlight
  347. },
  348. isReply () {
  349. return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
  350. },
  351. replyToName () {
  352. if (this.status.in_reply_to_screen_name) {
  353. return this.status.in_reply_to_screen_name
  354. } else {
  355. const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
  356. return user && user.screen_name_ui
  357. }
  358. },
  359. replySubject () {
  360. if (!this.status.summary) return ''
  361. const decodedSummary = unescape(this.status.summary)
  362. const behavior = this.mergedConfig.subjectLineBehavior
  363. const startsWithRe = decodedSummary.match(/^re[: ]/i)
  364. if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
  365. return decodedSummary
  366. } else if (behavior === 'email') {
  367. return 're: '.concat(decodedSummary)
  368. } else if (behavior === 'noop') {
  369. return ''
  370. }
  371. },
  372. combinedFavsAndRepeatsUsers () {
  373. // Use the status from the global status repository since favs and repeats are saved in it
  374. const combinedUsers = [].concat(
  375. this.statusFromGlobalRepository.favoritedBy,
  376. this.statusFromGlobalRepository.rebloggedBy
  377. )
  378. return uniqBy(combinedUsers, 'id')
  379. },
  380. tags () {
  381. // eslint-disable-next-line no-prototype-builtins
  382. return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
  383. },
  384. hidePostStats () {
  385. return this.mergedConfig.hidePostStats
  386. },
  387. shouldDisplayFavsAndRepeats () {
  388. return !this.hidePostStats && this.isFocused && (this.combinedFavsAndRepeatsUsers.length > 0 || this.statusFromGlobalRepository.quotes_count)
  389. },
  390. muteBotStatuses () {
  391. return this.mergedConfig.muteBotStatuses
  392. },
  393. muteSensitiveStatuses () {
  394. return this.mergedConfig.muteSensitiveStatuses
  395. },
  396. hideBotIndication () {
  397. return this.mergedConfig.hideBotIndication
  398. },
  399. currentUser () {
  400. return this.$store.state.users.currentUser
  401. },
  402. mergedConfig () {
  403. return this.$store.getters.mergedConfig
  404. },
  405. isSuspendable () {
  406. return !this.replying && this.mediaPlaying.length === 0
  407. },
  408. inThreadForest () {
  409. return !!this.controlledThreadDisplayStatus
  410. },
  411. threadShowing () {
  412. return this.controlledThreadDisplayStatus === 'showing'
  413. },
  414. visibilityLocalized () {
  415. return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
  416. },
  417. isEdited () {
  418. return this.status.edited_at !== null
  419. },
  420. editingAvailable () {
  421. return this.$store.state.instance.editingAvailable
  422. },
  423. hasVisibleQuote () {
  424. return this.status.quote_url && this.status.quote_visible
  425. },
  426. hasInvisibleQuote () {
  427. return this.status.quote_url && !this.status.quote_visible
  428. },
  429. quotedStatus () {
  430. return this.status.quote_id ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] : undefined
  431. },
  432. shouldDisplayQuote () {
  433. return this.quotedStatus && this.displayQuote
  434. },
  435. scrobblePresent () {
  436. if (this.mergedConfig.hideScrobbles) return false
  437. if (!this.status.user.latestScrobble) return false
  438. const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0]
  439. const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0]
  440. let multiplier = 60 * 1000 // minutes is smallest unit
  441. switch (unit) {
  442. case 'm':
  443. break
  444. case 'h':
  445. multiplier *= 60 // hour
  446. break
  447. case 'd':
  448. multiplier *= 60 // hour
  449. multiplier *= 24 // day
  450. break
  451. }
  452. const maxAge = Number(value) * multiplier
  453. const createdAt = Date.parse(this.status.user.latestScrobble.created_at)
  454. const age = Date.now() - createdAt
  455. if (age > maxAge) return false
  456. return this.status.user.latestScrobble.artist
  457. },
  458. scrobble () {
  459. return this.status.user.latestScrobble
  460. }
  461. },
  462. methods: {
  463. visibilityIcon (visibility) {
  464. switch (visibility) {
  465. case 'private':
  466. return 'lock'
  467. case 'unlisted':
  468. return 'lock-open'
  469. case 'direct':
  470. return 'envelope'
  471. default:
  472. return 'globe'
  473. }
  474. },
  475. showError (error) {
  476. this.error = error
  477. },
  478. clearError () {
  479. this.$emit('interacted')
  480. this.error = undefined
  481. },
  482. toggleReplying () {
  483. this.$emit('interacted')
  484. if (this.replying) {
  485. this.$refs.postStatusForm.requestClose()
  486. } else {
  487. this.doToggleReplying()
  488. }
  489. },
  490. doToggleReplying () {
  491. controlledOrUncontrolledToggle(this, 'replying')
  492. },
  493. gotoOriginal (id) {
  494. if (this.inConversation) {
  495. this.$emit('goto', id)
  496. }
  497. },
  498. toggleExpanded () {
  499. this.$emit('toggleExpanded')
  500. },
  501. toggleMute () {
  502. this.unmuted = !this.unmuted
  503. },
  504. toggleUserExpanded () {
  505. this.userExpanded = !this.userExpanded
  506. },
  507. generateUserProfileLink (id, name) {
  508. return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
  509. },
  510. addMediaPlaying (id) {
  511. controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
  512. },
  513. removeMediaPlaying (id) {
  514. controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
  515. },
  516. setHeadTailLinks (headTailLinks) {
  517. this.headTailLinks = headTailLinks
  518. },
  519. toggleThreadDisplay () {
  520. this.controlledToggleThreadDisplay()
  521. },
  522. scrollIfHighlighted (highlightId) {
  523. const id = highlightId
  524. if (this.status.id === id) {
  525. const rect = this.$el.getBoundingClientRect()
  526. if (rect.top < 100) {
  527. // Post is above screen, match its top to screen top
  528. window.scrollBy(0, rect.top - 100)
  529. } else if (rect.height >= (window.innerHeight - 50)) {
  530. // Post we want to see is taller than screen so match its top to screen top
  531. window.scrollBy(0, rect.top - 100)
  532. } else if (rect.bottom > window.innerHeight - 50) {
  533. // Post is below screen, match its bottom to screen bottom
  534. window.scrollBy(0, rect.bottom - window.innerHeight + 50)
  535. }
  536. }
  537. },
  538. toggleDisplayQuote () {
  539. if (this.shouldDisplayQuote) {
  540. this.displayQuote = false
  541. } else if (!this.quotedStatus) {
  542. this.$store.dispatch('fetchStatus', this.status.quote_id)
  543. .then(() => {
  544. this.displayQuote = true
  545. })
  546. } else {
  547. this.displayQuote = true
  548. }
  549. }
  550. },
  551. watch: {
  552. highlight: function (id) {
  553. this.scrollIfHighlighted(id)
  554. },
  555. 'status.repeat_num': function (num) {
  556. // refetch repeats when repeat_num is changed in any way
  557. if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {
  558. this.$store.dispatch('fetchRepeats', this.status.id)
  559. }
  560. },
  561. 'status.fave_num': function (num) {
  562. // refetch favs when fave_num is changed in any way
  563. if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
  564. this.$store.dispatch('fetchFavs', this.status.id)
  565. }
  566. },
  567. isSuspendable: function (val) {
  568. this.suspendable = val
  569. }
  570. }
  571. }
  572. export default Status