import statusPoster from '../../services/status_poster/status_poster.service.js' import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji-input/emoji-input.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import Completion from '../../services/completion/completion.js' import { take, filter, reject, map, uniqBy } from 'lodash' const buildMentionsString = ({user, attentions}, currentUser) => { let allAttentions = [...attentions] allAttentions.unshift(user) allAttentions = uniqBy(allAttentions, 'id') allAttentions = reject(allAttentions, {id: currentUser.id}) let mentions = map(allAttentions, (attention) => { return `@${attention.screen_name}` }) return mentions.length > 0 ? mentions.join(' ') + ' ' : '' } const PostStatusForm = { props: [ 'replyTo', 'repliedUser', 'attentions', 'copyMessageScope', 'subject' ], components: { MediaUpload, ScopeSelector, EmojiInput }, mounted () { this.resize(this.$refs.textarea) const textLength = this.$refs.textarea.value.length this.$refs.textarea.setSelectionRange(textLength, textLength) if (this.replyTo) { this.$refs.textarea.focus() } }, data () { const preset = this.$route.query.message let statusText = preset || '' const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined' ? this.$store.state.instance.scopeCopy : this.$store.state.config.scopeCopy if (this.replyTo) { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } const scope = (this.copyMessageScope && scopeCopy || this.copyMessageScope === 'direct') ? this.copyMessageScope : this.$store.state.users.currentUser.default_scope const contentType = typeof this.$store.state.config.postContentType === 'undefined' ? this.$store.state.instance.postContentType : this.$store.state.config.postContentType return { dropFiles: [], submitDisabled: false, error: null, posting: false, highlighted: 0, newStatus: { spoilerText: this.subject || '', status: statusText, nsfw: false, files: [], visibility: scope, contentType }, caret: 0 } }, computed: { candidates () { const firstchar = this.textAtCaret.charAt(0) if (firstchar === '@') { const query = this.textAtCaret.slice(1).toUpperCase() const matchedUsers = filter(this.users, (user) => { return user.screen_name.toUpperCase().startsWith(query) || user.name && user.name.toUpperCase().startsWith(query) }) if (matchedUsers.length <= 0) { return false } // eslint-disable-next-line camelcase return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({ // eslint-disable-next-line camelcase screen_name: `@${screen_name}`, name: name, img: profile_image_url_original, highlighted: index === this.highlighted })) } else if (firstchar === ':') { if (this.textAtCaret === ':') { return } const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1))) if (matchedEmoji.length <= 0) { return false } return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({ screen_name: `:${shortcode}:`, name: '', utf: utf || '', // eslint-disable-next-line camelcase img: utf ? '' : this.$store.state.instance.server + image_url, highlighted: index === this.highlighted })) } else { return false } }, textAtCaret () { return (this.wordAtCaret || {}).word || '' }, wordAtCaret () { const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {} return word }, users () { return this.$store.state.users.users }, userDefaultScope () { return this.$store.state.users.currentUser.default_scope }, showAllScopes () { const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined' ? this.$store.state.instance.minimalScopesMode : this.$store.state.config.minimalScopesMode return !minimalScopesMode }, emoji () { return this.$store.state.instance.emoji || [] }, customEmoji () { return this.$store.state.instance.customEmoji || [] }, statusLength () { return this.newStatus.status.length }, spoilerTextLength () { return this.newStatus.spoilerText.length }, statusLengthLimit () { return this.$store.state.instance.textlimit }, hasStatusLengthLimit () { return this.statusLengthLimit > 0 }, charactersLeft () { return this.statusLengthLimit - (this.statusLength + this.spoilerTextLength) }, isOverLengthLimit () { return this.hasStatusLengthLimit && (this.charactersLeft < 0) }, minimalScopesMode () { return this.$store.state.instance.minimalScopesMode }, alwaysShowSubject () { if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') { return this.$store.state.config.alwaysShowSubjectInput } else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') { return this.$store.state.instance.alwaysShowSubjectInput } else { return true } }, postFormats () { return this.$store.state.instance.postFormats || [] }, safeDMEnabled () { return this.$store.state.instance.safeDM }, hideScopeNotice () { return this.$store.state.config.hideScopeNotice } }, methods: { replace (replacement) { this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) const el = this.$el.querySelector('textarea') el.focus() this.caret = 0 }, replaceCandidate (e) { const len = this.candidates.length || 0 if (this.textAtCaret === ':' || e.ctrlKey) { return } if (len > 0) { e.preventDefault() const candidate = this.candidates[this.highlighted] const replacement = candidate.utf || (candidate.screen_name + ' ') this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) const el = this.$el.querySelector('textarea') el.focus() this.caret = 0 this.highlighted = 0 } }, cycleBackward (e) { const len = this.candidates.length || 0 if (len > 0) { e.preventDefault() this.highlighted -= 1 if (this.highlighted < 0) { this.highlighted = this.candidates.length - 1 } } else { this.highlighted = 0 } }, cycleForward (e) { const len = this.candidates.length || 0 if (len > 0) { if (e.shiftKey) { return } e.preventDefault() this.highlighted += 1 if (this.highlighted >= len) { this.highlighted = 0 } } else { this.highlighted = 0 } }, onKeydown (e) { e.stopPropagation() }, setCaret ({target: {selectionStart}}) { this.caret = selectionStart }, postStatus (newStatus) { if (this.posting) { return } if (this.submitDisabled) { return } if (this.newStatus.status === '') { if (this.newStatus.files.length > 0) { this.newStatus.status = '\u200b' // hack } else { this.error = 'Cannot post an empty status with no files' return } } this.posting = true statusPoster.postStatus({ status: newStatus.status, spoilerText: newStatus.spoilerText || null, visibility: newStatus.visibility, sensitive: newStatus.nsfw, media: newStatus.files, store: this.$store, inReplyToStatusId: this.replyTo, contentType: newStatus.contentType }).then((data) => { if (!data.error) { this.newStatus = { status: '', spoilerText: '', files: [], visibility: newStatus.visibility, contentType: newStatus.contentType } this.$refs.mediaUpload.clearFile() this.$emit('posted') let el = this.$el.querySelector('textarea') el.style.height = 'auto' el.style.height = undefined this.error = null } else { this.error = data.error } this.posting = false }) }, addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) this.enableSubmit() }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) this.newStatus.files.splice(index, 1) }, uploadFailed (errString, templateArgs) { templateArgs = templateArgs || {} this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs) this.enableSubmit() }, disableSubmit () { this.submitDisabled = true }, enableSubmit () { this.submitDisabled = false }, type (fileInfo) { return fileTypeService.fileType(fileInfo.mimetype) }, paste (e) { if (e.clipboardData.files.length > 0) { // prevent pasting of file as text e.preventDefault() // Strangely, files property gets emptied after event propagation // Trying to wrap it in array doesn't work. Plus I doubt it's possible // to hold more than one file in clipboard. this.dropFiles = [e.clipboardData.files[0]] } }, fileDrop (e) { if (e.dataTransfer.files.length > 0) { e.preventDefault() // allow dropping text like before this.dropFiles = e.dataTransfer.files } }, fileDrag (e) { e.dataTransfer.dropEffect = 'copy' }, resize (e) { const target = e.target || e if (!(target instanceof window.Element)) { return } const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) // Auto is needed to make textbox shrink when removing lines target.style.height = 'auto' target.style.height = `${target.scrollHeight - vertPadding}px` if (target.value === '') { target.style.height = null } }, clearError () { this.error = null }, changeVis (visibility) { this.newStatus.visibility = visibility }, dismissScopeNotice () { this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true }) } } } export default PostStatusForm