logo

pleroma-fe

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

post_status_form.js (27443B)


  1. import statusPoster from '../../services/status_poster/status_poster.service.js'
  2. import genRandomSeed from '../../services/random_seed/random_seed.service.js'
  3. import MediaUpload from '../media_upload/media_upload.vue'
  4. import ScopeSelector from '../scope_selector/scope_selector.vue'
  5. import EmojiInput from '../emoji_input/emoji_input.vue'
  6. import PollForm from '../poll/poll_form.vue'
  7. import Attachment from '../attachment/attachment.vue'
  8. import Gallery from 'src/components/gallery/gallery.vue'
  9. import StatusContent from '../status_content/status_content.vue'
  10. import Popover from 'src/components/popover/popover.vue'
  11. import fileTypeService from '../../services/file_type/file_type.service.js'
  12. import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
  13. import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
  14. import { pollFormToMasto } from 'src/services/poll/poll.service.js'
  15. import { reject, map, uniqBy, debounce } from 'lodash'
  16. import suggestor from '../emoji_input/suggestor.js'
  17. import { mapGetters } from 'vuex'
  18. import { mapState, mapActions } from 'pinia'
  19. import Checkbox from '../checkbox/checkbox.vue'
  20. import Select from '../select/select.vue'
  21. import DraftCloser from 'src/components/draft_closer/draft_closer.vue'
  22. import { library } from '@fortawesome/fontawesome-svg-core'
  23. import {
  24. faSmileBeam,
  25. faPollH,
  26. faUpload,
  27. faBan,
  28. faTimes,
  29. faCircleNotch,
  30. faChevronDown,
  31. faChevronLeft,
  32. faChevronRight
  33. } from '@fortawesome/free-solid-svg-icons'
  34. import { useInterfaceStore } from 'src/stores/interface.js'
  35. import { useMediaViewerStore } from 'src/stores/media_viewer.js'
  36. library.add(
  37. faSmileBeam,
  38. faPollH,
  39. faUpload,
  40. faBan,
  41. faTimes,
  42. faCircleNotch,
  43. faChevronDown,
  44. faChevronLeft,
  45. faChevronRight
  46. )
  47. const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
  48. let allAttentions = [...attentions]
  49. allAttentions.unshift(user)
  50. allAttentions = uniqBy(allAttentions, 'id')
  51. allAttentions = reject(allAttentions, { id: currentUser.id })
  52. const mentions = map(allAttentions, (attention) => {
  53. return `@${attention.screen_name}`
  54. })
  55. return mentions.length > 0 ? mentions.join(' ') + ' ' : ''
  56. }
  57. // Converts a string with px to a number like '2px' -> 2
  58. const pxStringToNumber = (str) => {
  59. return Number(str.substring(0, str.length - 2))
  60. }
  61. const typeAndRefId = ({ replyTo, profileMention, statusId }) => {
  62. if (replyTo) {
  63. return ['reply', replyTo]
  64. } else if (profileMention) {
  65. return ['mention', profileMention]
  66. } else if (statusId) {
  67. return ['edit', statusId]
  68. } else {
  69. return ['new', '']
  70. }
  71. }
  72. const PostStatusForm = {
  73. props: [
  74. 'statusId',
  75. 'statusText',
  76. 'statusIsSensitive',
  77. 'statusPoll',
  78. 'statusFiles',
  79. 'statusMediaDescriptions',
  80. 'statusScope',
  81. 'statusContentType',
  82. 'replyTo',
  83. 'repliedUser',
  84. 'attentions',
  85. 'copyMessageScope',
  86. 'subject',
  87. 'disableSubject',
  88. 'disableScopeSelector',
  89. 'disableVisibilitySelector',
  90. 'disableNotice',
  91. 'disableLockWarning',
  92. 'disablePolls',
  93. 'disableSensitivityCheckbox',
  94. 'disableSubmit',
  95. 'disablePreview',
  96. 'disableDraft',
  97. 'hideDraft',
  98. 'closeable',
  99. 'placeholder',
  100. 'maxHeight',
  101. 'postHandler',
  102. 'preserveFocus',
  103. 'autoFocus',
  104. 'fileLimit',
  105. 'submitOnEnter',
  106. 'emojiPickerPlacement',
  107. 'optimisticPosting',
  108. 'profileMention',
  109. 'draftId'
  110. ],
  111. emits: [
  112. 'posted',
  113. 'draft-done',
  114. 'resize',
  115. 'mediaplay',
  116. 'mediapause',
  117. 'can-close',
  118. 'update'
  119. ],
  120. components: {
  121. MediaUpload,
  122. EmojiInput,
  123. PollForm,
  124. ScopeSelector,
  125. Checkbox,
  126. Select,
  127. Attachment,
  128. StatusContent,
  129. Gallery,
  130. DraftCloser,
  131. Popover
  132. },
  133. mounted () {
  134. this.updateIdempotencyKey()
  135. this.resize(this.$refs.textarea)
  136. if (this.replyTo) {
  137. const textLength = this.$refs.textarea.value.length
  138. this.$refs.textarea.setSelectionRange(textLength, textLength)
  139. }
  140. if (this.replyTo || this.autoFocus) {
  141. this.$refs.textarea.focus()
  142. }
  143. },
  144. data () {
  145. const preset = this.$route.query.message
  146. let statusText = preset || ''
  147. const { scopeCopy } = this.$store.getters.mergedConfig
  148. const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, profileMention: this.profileMention && this.repliedUser?.id, statusId: this.statusId })
  149. // If we are starting a new post, do not associate it with old drafts
  150. let statusParams = !this.disableDraft && (this.draftId || statusType !== 'new') ? this.getDraft(statusType, refId) : null
  151. if (!statusParams) {
  152. if (statusType === 'reply' || statusType === 'mention') {
  153. const currentUser = this.$store.state.users.currentUser
  154. statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
  155. }
  156. const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
  157. ? this.copyMessageScope
  158. : this.$store.state.users.currentUser.default_scope
  159. const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
  160. statusParams = {
  161. type: statusType,
  162. refId,
  163. spoilerText: this.subject || '',
  164. status: statusText,
  165. nsfw: !!sensitiveByDefault,
  166. files: [],
  167. poll: {},
  168. hasPoll: false,
  169. mediaDescriptions: {},
  170. visibility: scope,
  171. contentType,
  172. quoting: false
  173. }
  174. if (statusType === 'edit') {
  175. const statusContentType = this.statusContentType || contentType
  176. statusParams = {
  177. type: statusType,
  178. refId,
  179. spoilerText: this.subject || '',
  180. status: this.statusText || '',
  181. nsfw: this.statusIsSensitive || !!sensitiveByDefault,
  182. files: this.statusFiles || [],
  183. poll: this.statusPoll || {},
  184. hasPoll: false,
  185. mediaDescriptions: this.statusMediaDescriptions || {},
  186. visibility: this.statusScope || scope,
  187. contentType: statusContentType
  188. }
  189. }
  190. }
  191. return {
  192. randomSeed: genRandomSeed(),
  193. dropFiles: [],
  194. uploadingFiles: false,
  195. error: null,
  196. posting: false,
  197. highlighted: 0,
  198. newStatus: statusParams,
  199. caret: 0,
  200. showDropIcon: 'hide',
  201. dropStopTimeout: null,
  202. preview: null,
  203. previewLoading: false,
  204. emojiInputShown: false,
  205. idempotencyKey: '',
  206. saveInhibited: true,
  207. saveable: false
  208. }
  209. },
  210. computed: {
  211. users () {
  212. return this.$store.state.users.users
  213. },
  214. userDefaultScope () {
  215. return this.$store.state.users.currentUser.default_scope
  216. },
  217. showAllScopes () {
  218. return !this.mergedConfig.minimalScopesMode
  219. },
  220. hideExtraActions () {
  221. return this.disableDraft || this.hideDraft
  222. },
  223. emojiUserSuggestor () {
  224. return suggestor({
  225. emoji: [
  226. ...this.$store.getters.standardEmojiList,
  227. ...this.$store.state.instance.customEmoji
  228. ],
  229. store: this.$store
  230. })
  231. },
  232. emojiSuggestor () {
  233. return suggestor({
  234. emoji: [
  235. ...this.$store.getters.standardEmojiList,
  236. ...this.$store.state.instance.customEmoji
  237. ]
  238. })
  239. },
  240. emoji () {
  241. return this.$store.getters.standardEmojiList || []
  242. },
  243. customEmoji () {
  244. return this.$store.state.instance.customEmoji || []
  245. },
  246. statusLength () {
  247. return this.newStatus.status.length
  248. },
  249. spoilerTextLength () {
  250. return this.newStatus.spoilerText.length
  251. },
  252. statusLengthLimit () {
  253. return this.$store.state.instance.textlimit
  254. },
  255. hasStatusLengthLimit () {
  256. return this.statusLengthLimit > 0
  257. },
  258. charactersLeft () {
  259. return this.statusLengthLimit - (this.statusLength + this.spoilerTextLength)
  260. },
  261. isOverLengthLimit () {
  262. return this.hasStatusLengthLimit && (this.charactersLeft < 0)
  263. },
  264. minimalScopesMode () {
  265. return this.$store.state.instance.minimalScopesMode
  266. },
  267. alwaysShowSubject () {
  268. return this.mergedConfig.alwaysShowSubjectInput
  269. },
  270. postFormats () {
  271. return this.$store.state.instance.postFormats || []
  272. },
  273. safeDMEnabled () {
  274. return this.$store.state.instance.safeDM
  275. },
  276. pollsAvailable () {
  277. return this.$store.state.instance.pollsAvailable &&
  278. this.$store.state.instance.pollLimits.max_options >= 2 &&
  279. this.disablePolls !== true
  280. },
  281. hideScopeNotice () {
  282. return this.disableNotice || this.$store.getters.mergedConfig.hideScopeNotice
  283. },
  284. pollContentError () {
  285. return this.pollFormVisible &&
  286. this.newStatus.poll &&
  287. this.newStatus.poll.error
  288. },
  289. showPreview () {
  290. return !this.disablePreview && (!!this.preview || this.previewLoading)
  291. },
  292. emptyStatus () {
  293. return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
  294. },
  295. uploadFileLimitReached () {
  296. return this.newStatus.files.length >= this.fileLimit
  297. },
  298. isEdit () {
  299. return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
  300. },
  301. quotable () {
  302. if (!this.$store.state.instance.quotingAvailable) {
  303. return false
  304. }
  305. if (!this.replyTo) {
  306. return false
  307. }
  308. const repliedStatus = this.$store.state.statuses.allStatusesObject[this.replyTo]
  309. if (!repliedStatus) {
  310. return false
  311. }
  312. if (repliedStatus.visibility === 'public' ||
  313. repliedStatus.visibility === 'unlisted' ||
  314. repliedStatus.visibility === 'local') {
  315. return true
  316. } else if (repliedStatus.visibility === 'private') {
  317. return repliedStatus.user.id === this.$store.state.users.currentUser.id
  318. }
  319. return false
  320. },
  321. debouncedMaybeAutoSaveDraft () {
  322. return debounce(this.maybeAutoSaveDraft, 3000)
  323. },
  324. pollFormVisible () {
  325. return this.newStatus.hasPoll
  326. },
  327. shouldAutoSaveDraft () {
  328. return this.$store.getters.mergedConfig.autoSaveDraft
  329. },
  330. autoSaveState () {
  331. if (this.saveable) {
  332. return this.$t('post_status.auto_save_saving')
  333. } else if (this.newStatus.id) {
  334. return this.$t('post_status.auto_save_saved')
  335. } else {
  336. return this.$t('post_status.auto_save_nothing_new')
  337. }
  338. },
  339. safeToSaveDraft () {
  340. console.log('safe', (
  341. this.newStatus.status ||
  342. this.newStatus.spoilerText ||
  343. this.newStatus.files?.length ||
  344. this.newStatus.hasPoll
  345. ) && this.saveable)
  346. return (
  347. this.newStatus.status ||
  348. this.newStatus.spoilerText ||
  349. this.newStatus.files?.length ||
  350. this.newStatus.hasPoll
  351. ) && this.saveable
  352. },
  353. hasEmptyDraft () {
  354. return this.newStatus.id && !(
  355. this.newStatus.status ||
  356. this.newStatus.spoilerText ||
  357. this.newStatus.files?.length ||
  358. this.newStatus.hasPoll
  359. )
  360. },
  361. ...mapGetters(['mergedConfig']),
  362. ...mapState(useInterfaceStore, {
  363. mobileLayout: store => store.mobileLayout
  364. })
  365. },
  366. watch: {
  367. newStatus: {
  368. deep: true,
  369. handler () {
  370. this.statusChanged()
  371. }
  372. },
  373. saveable (val) {
  374. // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes
  375. // MDN says we'd better add the beforeunload event listener only when needed, and remove it when it's no longer needed
  376. if (val) {
  377. this.addBeforeUnloadListener()
  378. } else {
  379. this.removeBeforeUnloadListener()
  380. }
  381. }
  382. },
  383. beforeUnmount () {
  384. this.maybeAutoSaveDraft()
  385. this.removeBeforeUnloadListener()
  386. },
  387. methods: {
  388. ...mapActions(useMediaViewerStore, ['increment']),
  389. statusChanged () {
  390. this.autoPreview()
  391. this.updateIdempotencyKey()
  392. this.debouncedMaybeAutoSaveDraft()
  393. this.saveable = true
  394. this.saveInhibited = false
  395. },
  396. clearStatus () {
  397. const newStatus = this.newStatus
  398. this.saveInhibited = true
  399. this.newStatus = {
  400. status: '',
  401. spoilerText: '',
  402. files: [],
  403. visibility: newStatus.visibility,
  404. contentType: newStatus.contentType,
  405. poll: {},
  406. hasPoll: false,
  407. mediaDescriptions: {},
  408. quoting: false
  409. }
  410. this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
  411. this.clearPollForm()
  412. if (this.preserveFocus) {
  413. this.$nextTick(() => {
  414. this.$refs.textarea.focus()
  415. })
  416. }
  417. const el = this.$el.querySelector('textarea')
  418. el.style.height = 'auto'
  419. el.style.height = undefined
  420. this.error = null
  421. if (this.preview) this.previewStatus()
  422. this.saveable = false
  423. },
  424. async postStatus (event, newStatus) {
  425. if (this.posting && !this.optimisticPosting) { return }
  426. if (this.disableSubmit) { return }
  427. if (this.emojiInputShown) { return }
  428. if (this.submitOnEnter) {
  429. event.stopPropagation()
  430. event.preventDefault()
  431. }
  432. if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
  433. if (this.emptyStatus) {
  434. this.error = this.$t('post_status.empty_status_error')
  435. return
  436. }
  437. const poll = this.newStatus.hasPoll ? pollFormToMasto(this.newStatus.poll) : {}
  438. if (this.pollContentError) {
  439. this.error = this.pollContentError
  440. return
  441. }
  442. this.posting = true
  443. try {
  444. await this.setAllMediaDescriptions()
  445. } catch {
  446. this.error = this.$t('post_status.media_description_error')
  447. this.posting = false
  448. return
  449. }
  450. const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
  451. const postingOptions = {
  452. status: newStatus.status,
  453. spoilerText: newStatus.spoilerText || null,
  454. visibility: newStatus.visibility,
  455. sensitive: newStatus.nsfw,
  456. media: newStatus.files,
  457. store: this.$store,
  458. [replyOrQuoteAttr]: this.replyTo,
  459. contentType: newStatus.contentType,
  460. poll,
  461. idempotencyKey: this.idempotencyKey
  462. }
  463. const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus
  464. postHandler(postingOptions).then((data) => {
  465. if (!data.error) {
  466. this.abandonDraft()
  467. this.clearStatus()
  468. this.$emit('posted', data)
  469. } else {
  470. this.error = data.error
  471. }
  472. this.posting = false
  473. })
  474. },
  475. previewStatus () {
  476. if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
  477. this.preview = { error: this.$t('post_status.preview_empty') }
  478. this.previewLoading = false
  479. return
  480. }
  481. const newStatus = this.newStatus
  482. this.previewLoading = true
  483. const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
  484. statusPoster.postStatus({
  485. status: newStatus.status,
  486. spoilerText: newStatus.spoilerText || null,
  487. visibility: newStatus.visibility,
  488. sensitive: newStatus.nsfw,
  489. media: [],
  490. store: this.$store,
  491. [replyOrQuoteAttr]: this.replyTo,
  492. contentType: newStatus.contentType,
  493. poll: {},
  494. preview: true
  495. }).then((data) => {
  496. // Don't apply preview if not loading, because it means
  497. // user has closed the preview manually.
  498. if (!this.previewLoading) return
  499. if (!data.error) {
  500. this.preview = data
  501. } else {
  502. this.preview = { error: data.error }
  503. }
  504. }).catch((error) => {
  505. this.preview = { error }
  506. }).finally(() => {
  507. this.previewLoading = false
  508. })
  509. },
  510. debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500),
  511. autoPreview () {
  512. if (!this.preview) return
  513. this.previewLoading = true
  514. this.debouncePreviewStatus()
  515. },
  516. closePreview () {
  517. this.preview = null
  518. this.previewLoading = false
  519. },
  520. togglePreview () {
  521. if (this.showPreview) {
  522. this.closePreview()
  523. } else {
  524. this.previewStatus()
  525. }
  526. },
  527. addMediaFile (fileInfo) {
  528. this.newStatus.files.push(fileInfo)
  529. this.$emit('resize', { delayed: true })
  530. },
  531. removeMediaFile (fileInfo) {
  532. const index = this.newStatus.files.indexOf(fileInfo)
  533. this.newStatus.files.splice(index, 1)
  534. this.$emit('resize')
  535. },
  536. editAttachment (fileInfo, newText) {
  537. this.newStatus.mediaDescriptions[fileInfo.id] = newText
  538. },
  539. shiftUpMediaFile (fileInfo) {
  540. const { files } = this.newStatus
  541. const index = this.newStatus.files.indexOf(fileInfo)
  542. files.splice(index, 1)
  543. files.splice(index - 1, 0, fileInfo)
  544. },
  545. shiftDnMediaFile (fileInfo) {
  546. const { files } = this.newStatus
  547. const index = this.newStatus.files.indexOf(fileInfo)
  548. files.splice(index, 1)
  549. files.splice(index + 1, 0, fileInfo)
  550. },
  551. uploadFailed (errString, templateArgs) {
  552. templateArgs = templateArgs || {}
  553. this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
  554. },
  555. startedUploadingFiles () {
  556. this.uploadingFiles = true
  557. },
  558. finishedUploadingFiles () {
  559. this.$emit('resize')
  560. this.uploadingFiles = false
  561. },
  562. type (fileInfo) {
  563. return fileTypeService.fileType(fileInfo.mimetype)
  564. },
  565. paste (e) {
  566. this.autoPreview()
  567. this.resize(e)
  568. if (e.clipboardData.files.length > 0) {
  569. // prevent pasting of file as text
  570. e.preventDefault()
  571. // Strangely, files property gets emptied after event propagation
  572. // Trying to wrap it in array doesn't work. Plus I doubt it's possible
  573. // to hold more than one file in clipboard.
  574. this.dropFiles = [e.clipboardData.files[0]]
  575. }
  576. },
  577. fileDrop (e) {
  578. if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
  579. e.preventDefault() // allow dropping text like before
  580. this.dropFiles = e.dataTransfer.files
  581. clearTimeout(this.dropStopTimeout)
  582. this.showDropIcon = 'hide'
  583. }
  584. },
  585. fileDragStop () {
  586. // The false-setting is done with delay because just using leave-events
  587. // directly caused unwanted flickering, this is not perfect either but
  588. // much less noticable.
  589. clearTimeout(this.dropStopTimeout)
  590. this.showDropIcon = 'fade'
  591. this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
  592. },
  593. fileDrag (e) {
  594. e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy'
  595. if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
  596. clearTimeout(this.dropStopTimeout)
  597. this.showDropIcon = 'show'
  598. }
  599. },
  600. onEmojiInputInput () {
  601. this.$nextTick(() => {
  602. this.resize(this.$refs.textarea)
  603. })
  604. },
  605. resize (e) {
  606. const target = e.target || e
  607. if (!(target instanceof window.Element)) { return }
  608. // Reset to default height for empty form, nothing else to do here.
  609. if (target.value === '') {
  610. target.style.height = null
  611. this.$emit('resize')
  612. return
  613. }
  614. const formRef = this.$refs.form
  615. const bottomRef = this.$refs.bottom
  616. /* Scroller is either `window` (replies in TL), sidebar (main post form,
  617. * replies in notifs) or mobile post form. Note that getting and setting
  618. * scroll is different for `Window` and `Element`s
  619. */
  620. const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom']
  621. const bottomBottomPadding = pxStringToNumber(bottomBottomPaddingStr)
  622. const scrollerRef = this.$el.closest('.column.-scrollable') ||
  623. this.$el.closest('.post-form-modal-view') ||
  624. window
  625. // Getting info about padding we have to account for, removing 'px' part
  626. const topPaddingStr = window.getComputedStyle(target)['padding-top']
  627. const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
  628. const topPadding = pxStringToNumber(topPaddingStr)
  629. const bottomPadding = pxStringToNumber(bottomPaddingStr)
  630. const vertPadding = topPadding + bottomPadding
  631. const oldHeight = pxStringToNumber(target.style.height)
  632. /* Explanation:
  633. *
  634. * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
  635. * scrollHeight returns element's scrollable content height, i.e. visible
  636. * element + overscrolled parts of it. We use it to determine when text
  637. * inside the textarea exceeded its height, so we can set height to prevent
  638. * overscroll, i.e. make textarea grow with the text. HOWEVER, since we
  639. * explicitly set new height, scrollHeight won't go below that, so we can't
  640. * SHRINK the textarea when there's extra space. To workaround that we set
  641. * height to 'auto' which makes textarea tiny again, so that scrollHeight
  642. * will match text height again. HOWEVER, shrinking textarea can screw with
  643. * the scroll since there might be not enough padding around form-bottom to even
  644. * warrant a scroll, so it will jump to 0 and refuse to move anywhere,
  645. * so we check current scroll position before shrinking and then restore it
  646. * with needed delta.
  647. */
  648. // this part has to be BEFORE the content size update
  649. const currentScroll = scrollerRef === window
  650. ? scrollerRef.scrollY
  651. : scrollerRef.scrollTop
  652. const scrollerHeight = scrollerRef === window
  653. ? scrollerRef.innerHeight
  654. : scrollerRef.offsetHeight
  655. const scrollerBottomBorder = currentScroll + scrollerHeight
  656. // BEGIN content size update
  657. target.style.height = 'auto'
  658. const heightWithoutPadding = Math.floor(target.scrollHeight - vertPadding)
  659. let newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
  660. // This is a bit of a hack to combat target.scrollHeight being different on every other input
  661. // on some browsers for whatever reason. Don't change the height if difference is 1px or less.
  662. if (Math.abs(newHeight - oldHeight) <= 1) {
  663. newHeight = oldHeight
  664. }
  665. target.style.height = `${newHeight}px`
  666. this.$emit('resize', newHeight)
  667. // END content size update
  668. // We check where the bottom border of form-bottom element is, this uses findOffset
  669. // to find offset relative to scrollable container (scroller)
  670. const bottomBottomBorder = bottomRef.offsetHeight + findOffset(bottomRef, scrollerRef).top + bottomBottomPadding
  671. const isBottomObstructed = scrollerBottomBorder < bottomBottomBorder
  672. const isFormBiggerThanScroller = scrollerHeight < formRef.offsetHeight
  673. const bottomChangeDelta = bottomBottomBorder - scrollerBottomBorder
  674. // The intention is basically this;
  675. // Keep form-bottom always visible so that submit button is in view EXCEPT
  676. // if form element bigger than scroller and caret isn't at the end, so that
  677. // if you scroll up and edit middle of text you won't get scrolled back to bottom
  678. const shouldScrollToBottom = isBottomObstructed &&
  679. !(isFormBiggerThanScroller &&
  680. this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
  681. const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
  682. const targetScroll = Math.round(currentScroll + totalDelta)
  683. if (scrollerRef === window) {
  684. scrollerRef.scroll(0, targetScroll)
  685. } else {
  686. scrollerRef.scrollTop = targetScroll
  687. }
  688. },
  689. showEmojiPicker () {
  690. this.$refs.textarea.focus()
  691. this.$refs['emoji-input'].triggerShowPicker()
  692. },
  693. clearError () {
  694. this.error = null
  695. },
  696. changeVis (visibility) {
  697. this.newStatus.visibility = visibility
  698. },
  699. togglePollForm () {
  700. this.newStatus.hasPoll = !this.newStatus.hasPoll
  701. },
  702. setPoll (poll) {
  703. this.newStatus.poll = poll
  704. },
  705. clearPollForm () {
  706. if (this.$refs.pollForm) {
  707. this.$refs.pollForm.clear()
  708. }
  709. },
  710. dismissScopeNotice () {
  711. this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
  712. },
  713. setMediaDescription (id) {
  714. const description = this.newStatus.mediaDescriptions[id]
  715. if (!description || description.trim() === '') return
  716. return statusPoster.setMediaDescription({ store: this.$store, id, description })
  717. },
  718. setAllMediaDescriptions () {
  719. const ids = this.newStatus.files.map(file => file.id)
  720. return Promise.all(ids.map(id => this.setMediaDescription(id)))
  721. },
  722. handleEmojiInputShow (value) {
  723. this.emojiInputShown = value
  724. },
  725. updateIdempotencyKey () {
  726. this.idempotencyKey = Date.now().toString()
  727. },
  728. openProfileTab () {
  729. useInterfaceStore().openSettingsModalTab('profile')
  730. },
  731. propsToNative (props) {
  732. return propsToNative(props)
  733. },
  734. saveDraft () {
  735. if (!this.disableDraft &&
  736. !this.saveInhibited) {
  737. if (this.safeToSaveDraft) {
  738. return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus })
  739. .then(id => {
  740. if (this.newStatus.id !== id) {
  741. this.newStatus.id = id
  742. }
  743. this.saveable = false
  744. if (!this.shouldAutoSaveDraft) {
  745. this.clearStatus()
  746. this.$emit('draft-done')
  747. }
  748. })
  749. } else if (this.hasEmptyDraft) {
  750. // There is a draft, but there is nothing in it, clear it
  751. return this.abandonDraft()
  752. .then(() => {
  753. this.saveable = false
  754. if (!this.shouldAutoSaveDraft) {
  755. this.clearStatus()
  756. this.$emit('draft-done')
  757. }
  758. })
  759. }
  760. }
  761. return Promise.resolve()
  762. },
  763. maybeAutoSaveDraft () {
  764. if (this.shouldAutoSaveDraft) {
  765. this.saveDraft(false)
  766. }
  767. },
  768. abandonDraft () {
  769. return this.$store.dispatch('abandonDraft', { id: this.newStatus.id })
  770. },
  771. getDraft (statusType, refId) {
  772. const maybeDraft = this.$store.state.drafts.drafts[this.draftId]
  773. if (this.draftId && maybeDraft) {
  774. return maybeDraft
  775. } else {
  776. const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId)
  777. if (existingDrafts.length) {
  778. return existingDrafts[0]
  779. }
  780. }
  781. // No draft available, fall back
  782. },
  783. requestClose () {
  784. if (!this.saveable) {
  785. this.$emit('can-close')
  786. } else {
  787. this.$refs.draftCloser.requestClose()
  788. }
  789. },
  790. saveAndCloseDraft () {
  791. this.saveDraft().then(() => {
  792. this.$emit('can-close')
  793. })
  794. },
  795. discardAndCloseDraft () {
  796. this.abandonDraft().then(() => {
  797. this.$emit('can-close')
  798. })
  799. },
  800. addBeforeUnloadListener () {
  801. this._beforeUnloadListener ||= () => {
  802. this.saveDraft()
  803. }
  804. window.addEventListener('beforeunload', this._beforeUnloadListener)
  805. },
  806. removeBeforeUnloadListener () {
  807. if (this._beforeUnloadListener) {
  808. window.removeEventListener('beforeunload', this._beforeUnloadListener)
  809. }
  810. }
  811. }
  812. }
  813. export default PostStatusForm