forked from AkkomaGang/akkoma-fe
a3305799c7
* upstream/develop: (42 commits) Fix formatting in oc.json avoid using global class fix logo moving bug when lightbox is open Reserve scrollbar gap when body scroll is locked setting display: initial makes trouble, instead, toggle display: none using classname lock body scroll add body-scroll-lock directive install body-scroll-lock wire up props with PostStatusModal rename component recover autofocusing behavior refactor MobilePostStatusModal using new PostStatusModal add new module and modal to post new status remove needless condition add mention button wire up user state with global store collapse fav/repeat notifications from muted users do not collapse thread muted posts in conversation detect thread-muted posts do not change word based muting logic ...
387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
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 PollForm from '../poll/poll_form.vue'
|
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
|
import { reject, map, uniqBy } from 'lodash'
|
|
import suggestor from '../emoji_input/suggestor.js'
|
|
|
|
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,
|
|
EmojiInput,
|
|
PollForm,
|
|
ScopeSelector
|
|
},
|
|
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: [],
|
|
poll: {},
|
|
visibility: scope,
|
|
contentType
|
|
},
|
|
caret: 0,
|
|
pollFormVisible: false
|
|
}
|
|
},
|
|
computed: {
|
|
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
|
|
},
|
|
emojiUserSuggestor () {
|
|
return suggestor({
|
|
emoji: [
|
|
...this.$store.state.instance.emoji,
|
|
...this.$store.state.instance.customEmoji
|
|
],
|
|
users: this.$store.state.users.users,
|
|
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
|
})
|
|
},
|
|
emojiSuggestor () {
|
|
return suggestor({
|
|
emoji: [
|
|
...this.$store.state.instance.emoji,
|
|
...this.$store.state.instance.customEmoji
|
|
]
|
|
})
|
|
},
|
|
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
|
|
},
|
|
pollsAvailable () {
|
|
return this.$store.state.instance.pollsAvailable &&
|
|
this.$store.state.instance.pollLimits.max_options >= 2
|
|
},
|
|
hideScopeNotice () {
|
|
return this.$store.state.config.hideScopeNotice
|
|
},
|
|
pollContentError () {
|
|
return this.pollFormVisible &&
|
|
this.newStatus.poll &&
|
|
this.newStatus.poll.error
|
|
}
|
|
},
|
|
methods: {
|
|
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
|
|
}
|
|
}
|
|
|
|
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
|
if (this.pollContentError) {
|
|
this.error = this.pollContentError
|
|
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,
|
|
poll
|
|
}).then((data) => {
|
|
if (!data.error) {
|
|
this.newStatus = {
|
|
status: '',
|
|
spoilerText: '',
|
|
files: [],
|
|
visibility: newStatus.visibility,
|
|
contentType: newStatus.contentType,
|
|
poll: {}
|
|
}
|
|
this.pollFormVisible = false
|
|
this.$refs.mediaUpload.clearFile()
|
|
this.clearPollForm()
|
|
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) {
|
|
this.resize(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'
|
|
},
|
|
onEmojiInputInput (e) {
|
|
this.$nextTick(() => {
|
|
this.resize(this.$refs['textarea'])
|
|
})
|
|
},
|
|
resize (e) {
|
|
const target = e.target || e
|
|
if (!(target instanceof window.Element)) { return }
|
|
|
|
// Reset to default height for empty form, nothing else to do here.
|
|
if (target.value === '') {
|
|
target.style.height = null
|
|
this.$refs['emoji-input'].resize()
|
|
return
|
|
}
|
|
|
|
const rootRef = this.$refs['root']
|
|
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
|
* replies in notifs) or mobile post form. Note that getting and setting
|
|
* scroll is different for `Window` and `Element`s
|
|
*/
|
|
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
|
this.$el.closest('.post-form-modal-view') ||
|
|
window
|
|
|
|
// Getting info about padding we have to account for, removing 'px' part
|
|
const topPaddingStr = window.getComputedStyle(target)['padding-top']
|
|
const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
|
|
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
|
|
const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))
|
|
const vertPadding = topPadding + bottomPadding
|
|
|
|
const oldHeightStr = target.style.height || ''
|
|
const oldHeight = Number(oldHeightStr.substring(0, oldHeightStr.length - 2))
|
|
|
|
/* Explanation:
|
|
*
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
|
* scrollHeight returns element's scrollable content height, i.e. visible
|
|
* element + overscrolled parts of it. We use it to determine when text
|
|
* inside the textarea exceeded its height, so we can set height to prevent
|
|
* overscroll, i.e. make textarea grow with the text. HOWEVER, since we
|
|
* explicitly set new height, scrollHeight won't go below that, so we can't
|
|
* SHRINK the textarea when there's extra space. To workaround that we set
|
|
* height to 'auto' which makes textarea tiny again, so that scrollHeight
|
|
* will match text height again. HOWEVER, shrinking textarea can screw with
|
|
* the scroll since there might be not enough padding around root to even
|
|
* warrant a scroll, so it will jump to 0 and refuse to move anywhere,
|
|
* so we check current scroll position before shrinking and then restore it
|
|
* with needed delta.
|
|
*/
|
|
|
|
// this part has to be BEFORE the content size update
|
|
const currentScroll = scrollerRef === window
|
|
? scrollerRef.scrollY
|
|
: scrollerRef.scrollTop
|
|
const scrollerHeight = scrollerRef === window
|
|
? scrollerRef.innerHeight
|
|
: scrollerRef.offsetHeight
|
|
const scrollerBottomBorder = currentScroll + scrollerHeight
|
|
|
|
// BEGIN content size update
|
|
target.style.height = 'auto'
|
|
const newHeight = target.scrollHeight - vertPadding
|
|
target.style.height = `${newHeight}px`
|
|
// END content size update
|
|
|
|
// We check where the bottom border of root element is, this uses findOffset
|
|
// to find offset relative to scrollable container (scroller)
|
|
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
|
|
|
const textareaSizeChangeDelta = newHeight - oldHeight || 0
|
|
const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
|
|
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
|
|
const totalDelta = textareaSizeChangeDelta +
|
|
(isBottomObstructed ? rootChangeDelta : 0)
|
|
|
|
const targetScroll = currentScroll + totalDelta
|
|
|
|
if (scrollerRef === window) {
|
|
scrollerRef.scroll(0, targetScroll)
|
|
} else {
|
|
scrollerRef.scrollTop = targetScroll
|
|
}
|
|
|
|
this.$refs['emoji-input'].resize()
|
|
},
|
|
showEmojiPicker () {
|
|
this.$refs['textarea'].focus()
|
|
this.$refs['emoji-input'].triggerShowPicker()
|
|
},
|
|
clearError () {
|
|
this.error = null
|
|
},
|
|
changeVis (visibility) {
|
|
this.newStatus.visibility = visibility
|
|
},
|
|
togglePollForm () {
|
|
this.pollFormVisible = !this.pollFormVisible
|
|
},
|
|
setPoll (poll) {
|
|
this.newStatus.poll = poll
|
|
},
|
|
clearPollForm () {
|
|
if (this.$refs.pollForm) {
|
|
this.$refs.pollForm.clear()
|
|
}
|
|
},
|
|
dismissScopeNotice () {
|
|
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
export default PostStatusForm
|