Merge remote-tracking branch 'origin/develop' into migrate/vuex-to-pinia
This commit is contained in:
commit
58e18d48df
489 changed files with 31167 additions and 9871 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||
import genRandomSeed from '../../services/random_seed/random_seed.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'
|
||||
|
|
@ -6,15 +7,18 @@ import PollForm from '../poll/poll_form.vue'
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import Gallery from 'src/components/gallery/gallery.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
|
||||
import { pollFormToMasto } from 'src/services/poll/poll.service.js'
|
||||
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||
import suggestor from '../emoji_input/suggestor.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Select from '../select/select.vue'
|
||||
import DraftCloser from 'src/components/draft_closer/draft_closer.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -23,7 +27,10 @@ import {
|
|||
faUpload,
|
||||
faBan,
|
||||
faTimes,
|
||||
faCircleNotch
|
||||
faCircleNotch,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from '../../stores/interface.js'
|
||||
|
||||
|
|
@ -33,7 +40,10 @@ library.add(
|
|||
faUpload,
|
||||
faBan,
|
||||
faTimes,
|
||||
faCircleNotch
|
||||
faCircleNotch,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight
|
||||
)
|
||||
|
||||
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
||||
|
|
@ -56,6 +66,18 @@ const pxStringToNumber = (str) => {
|
|||
return Number(str.substring(0, str.length - 2))
|
||||
}
|
||||
|
||||
const typeAndRefId = ({ replyTo, profileMention, statusId }) => {
|
||||
if (replyTo) {
|
||||
return ['reply', replyTo]
|
||||
} else if (profileMention) {
|
||||
return ['mention', profileMention]
|
||||
} else if (statusId) {
|
||||
return ['edit', statusId]
|
||||
} else {
|
||||
return ['new', '']
|
||||
}
|
||||
}
|
||||
|
||||
const PostStatusForm = {
|
||||
props: [
|
||||
'statusId',
|
||||
|
|
@ -80,6 +102,9 @@ const PostStatusForm = {
|
|||
'disableSensitivityCheckbox',
|
||||
'disableSubmit',
|
||||
'disablePreview',
|
||||
'disableDraft',
|
||||
'hideDraft',
|
||||
'closeable',
|
||||
'placeholder',
|
||||
'maxHeight',
|
||||
'postHandler',
|
||||
|
|
@ -88,13 +113,18 @@ const PostStatusForm = {
|
|||
'fileLimit',
|
||||
'submitOnEnter',
|
||||
'emojiPickerPlacement',
|
||||
'optimisticPosting'
|
||||
'optimisticPosting',
|
||||
'profileMention',
|
||||
'draftId'
|
||||
],
|
||||
emits: [
|
||||
'posted',
|
||||
'draft-done',
|
||||
'resize',
|
||||
'mediaplay',
|
||||
'mediapause'
|
||||
'mediapause',
|
||||
'can-close',
|
||||
'update'
|
||||
],
|
||||
components: {
|
||||
MediaUpload,
|
||||
|
|
@ -105,7 +135,9 @@ const PostStatusForm = {
|
|||
Select,
|
||||
Attachment,
|
||||
StatusContent,
|
||||
Gallery
|
||||
Gallery,
|
||||
DraftCloser,
|
||||
Popover
|
||||
},
|
||||
mounted () {
|
||||
this.updateIdempotencyKey()
|
||||
|
|
@ -126,43 +158,58 @@ const PostStatusForm = {
|
|||
|
||||
const { scopeCopy } = this.$store.getters.mergedConfig
|
||||
|
||||
if (this.replyTo) {
|
||||
const currentUser = this.$store.state.users.currentUser
|
||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, profileMention: this.profileMention && this.repliedUser?.id, statusId: this.statusId })
|
||||
|
||||
const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
|
||||
? this.copyMessageScope
|
||||
: this.$store.state.users.currentUser.default_scope
|
||||
// If we are starting a new post, do not associate it with old drafts
|
||||
let statusParams = !this.disableDraft && (this.draftId || statusType !== 'new') ? this.getDraft(statusType, refId) : null
|
||||
|
||||
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
||||
if (!statusParams) {
|
||||
if (statusType === 'reply' || statusType === 'mention') {
|
||||
const currentUser = this.$store.state.users.currentUser
|
||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
|
||||
let statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
status: statusText,
|
||||
nsfw: !!sensitiveByDefault,
|
||||
files: [],
|
||||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: scope,
|
||||
contentType
|
||||
}
|
||||
const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
|
||||
? this.copyMessageScope
|
||||
: this.$store.state.users.currentUser.default_scope
|
||||
|
||||
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
||||
|
||||
if (this.statusId) {
|
||||
const statusContentType = this.statusContentType || contentType
|
||||
statusParams = {
|
||||
type: statusType,
|
||||
refId,
|
||||
spoilerText: this.subject || '',
|
||||
status: this.statusText || '',
|
||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||
files: this.statusFiles || [],
|
||||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || scope,
|
||||
contentType: statusContentType
|
||||
status: statusText,
|
||||
nsfw: !!sensitiveByDefault,
|
||||
files: [],
|
||||
poll: {},
|
||||
hasPoll: false,
|
||||
mediaDescriptions: {},
|
||||
visibility: scope,
|
||||
contentType,
|
||||
quoting: false
|
||||
}
|
||||
|
||||
if (statusType === 'edit') {
|
||||
const statusContentType = this.statusContentType || contentType
|
||||
statusParams = {
|
||||
type: statusType,
|
||||
refId,
|
||||
spoilerText: this.subject || '',
|
||||
status: this.statusText || '',
|
||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||
files: this.statusFiles || [],
|
||||
poll: this.statusPoll || {},
|
||||
hasPoll: false,
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || scope,
|
||||
contentType: statusContentType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
randomSeed: genRandomSeed(),
|
||||
dropFiles: [],
|
||||
uploadingFiles: false,
|
||||
error: null,
|
||||
|
|
@ -170,13 +217,14 @@ const PostStatusForm = {
|
|||
highlighted: 0,
|
||||
newStatus: statusParams,
|
||||
caret: 0,
|
||||
pollFormVisible: false,
|
||||
showDropIcon: 'hide',
|
||||
dropStopTimeout: null,
|
||||
preview: null,
|
||||
previewLoading: false,
|
||||
emojiInputShown: false,
|
||||
idempotencyKey: ''
|
||||
idempotencyKey: '',
|
||||
saveInhibited: true,
|
||||
saveable: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -189,6 +237,9 @@ const PostStatusForm = {
|
|||
showAllScopes () {
|
||||
return !this.mergedConfig.minimalScopesMode
|
||||
},
|
||||
hideExtraActions () {
|
||||
return this.disableDraft || this.hideDraft
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
|
|
@ -267,6 +318,56 @@ const PostStatusForm = {
|
|||
isEdit () {
|
||||
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
||||
},
|
||||
quotable () {
|
||||
if (!this.$store.state.instance.quotingAvailable) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.replyTo) {
|
||||
return false
|
||||
}
|
||||
|
||||
const repliedStatus = this.$store.state.statuses.allStatusesObject[this.replyTo]
|
||||
if (!repliedStatus) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (repliedStatus.visibility === 'public' ||
|
||||
repliedStatus.visibility === 'unlisted' ||
|
||||
repliedStatus.visibility === 'local') {
|
||||
return true
|
||||
} else if (repliedStatus.visibility === 'private') {
|
||||
return repliedStatus.user.id === this.$store.state.users.currentUser.id
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
debouncedMaybeAutoSaveDraft () {
|
||||
return debounce(this.maybeAutoSaveDraft, 3000)
|
||||
},
|
||||
pollFormVisible () {
|
||||
return this.newStatus.hasPoll
|
||||
},
|
||||
shouldAutoSaveDraft () {
|
||||
return this.$store.getters.mergedConfig.autoSaveDraft
|
||||
},
|
||||
autoSaveState () {
|
||||
if (this.saveable) {
|
||||
return this.$t('post_status.auto_save_saving')
|
||||
} else if (this.newStatus.id) {
|
||||
return this.$t('post_status.auto_save_saved')
|
||||
} else {
|
||||
return this.$t('post_status.auto_save_nothing_new')
|
||||
}
|
||||
},
|
||||
safeToSaveDraft () {
|
||||
return (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
this.newStatus.files?.length ||
|
||||
this.newStatus.hasPoll
|
||||
) && this.saveable
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState(useInterfaceStore, {
|
||||
mobileLayout: store => store.mobileLayout
|
||||
|
|
@ -278,15 +379,32 @@ const PostStatusForm = {
|
|||
handler () {
|
||||
this.statusChanged()
|
||||
}
|
||||
},
|
||||
saveable (val) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes
|
||||
// MDN says we'd better add the beforeunload event listener only when needed, and remove it when it's no longer needed
|
||||
if (val) {
|
||||
this.addBeforeUnloadListener()
|
||||
} else {
|
||||
this.removeBeforeUnloadListener()
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.maybeAutoSaveDraft()
|
||||
this.removeBeforeUnloadListener()
|
||||
},
|
||||
methods: {
|
||||
statusChanged () {
|
||||
this.autoPreview()
|
||||
this.updateIdempotencyKey()
|
||||
this.debouncedMaybeAutoSaveDraft()
|
||||
this.saveable = true
|
||||
this.saveInhibited = false
|
||||
},
|
||||
clearStatus () {
|
||||
const newStatus = this.newStatus
|
||||
this.saveInhibited = true
|
||||
this.newStatus = {
|
||||
status: '',
|
||||
spoilerText: '',
|
||||
|
|
@ -294,9 +412,10 @@ const PostStatusForm = {
|
|||
visibility: newStatus.visibility,
|
||||
contentType: newStatus.contentType,
|
||||
poll: {},
|
||||
mediaDescriptions: {}
|
||||
hasPoll: false,
|
||||
mediaDescriptions: {},
|
||||
quoting: false
|
||||
}
|
||||
this.pollFormVisible = false
|
||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||
this.clearPollForm()
|
||||
if (this.preserveFocus) {
|
||||
|
|
@ -309,6 +428,7 @@ const PostStatusForm = {
|
|||
el.style.height = undefined
|
||||
this.error = null
|
||||
if (this.preview) this.previewStatus()
|
||||
this.saveable = false
|
||||
},
|
||||
async postStatus (event, newStatus, opts = {}) {
|
||||
if (this.posting && !this.optimisticPosting) { return }
|
||||
|
|
@ -326,7 +446,7 @@ const PostStatusForm = {
|
|||
return
|
||||
}
|
||||
|
||||
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
||||
const poll = this.newStatus.hasPoll ? pollFormToMasto(this.newStatus.poll) : {}
|
||||
if (this.pollContentError) {
|
||||
this.error = this.pollContentError
|
||||
return
|
||||
|
|
@ -342,6 +462,8 @@ const PostStatusForm = {
|
|||
return
|
||||
}
|
||||
|
||||
const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
|
||||
|
||||
const postingOptions = {
|
||||
status: newStatus.status,
|
||||
spoilerText: newStatus.spoilerText || null,
|
||||
|
|
@ -349,7 +471,7 @@ const PostStatusForm = {
|
|||
sensitive: newStatus.nsfw,
|
||||
media: newStatus.files,
|
||||
store: this.$store,
|
||||
inReplyToStatusId: this.replyTo,
|
||||
[replyOrQuoteAttr]: this.replyTo,
|
||||
contentType: newStatus.contentType,
|
||||
poll,
|
||||
idempotencyKey: this.idempotencyKey
|
||||
|
|
@ -359,6 +481,7 @@ const PostStatusForm = {
|
|||
|
||||
postHandler(postingOptions).then((data) => {
|
||||
if (!data.error) {
|
||||
this.abandonDraft()
|
||||
this.clearStatus()
|
||||
this.$emit('posted', data)
|
||||
} else {
|
||||
|
|
@ -375,6 +498,7 @@ const PostStatusForm = {
|
|||
}
|
||||
const newStatus = this.newStatus
|
||||
this.previewLoading = true
|
||||
const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
|
||||
statusPoster.postStatus({
|
||||
status: newStatus.status,
|
||||
spoilerText: newStatus.spoilerText || null,
|
||||
|
|
@ -382,7 +506,7 @@ const PostStatusForm = {
|
|||
sensitive: newStatus.nsfw,
|
||||
media: [],
|
||||
store: this.$store,
|
||||
inReplyToStatusId: this.replyTo,
|
||||
[replyOrQuoteAttr]: this.replyTo,
|
||||
contentType: newStatus.contentType,
|
||||
poll: {},
|
||||
preview: true
|
||||
|
|
@ -602,7 +726,7 @@ const PostStatusForm = {
|
|||
this.newStatus.visibility = visibility
|
||||
},
|
||||
togglePollForm () {
|
||||
this.pollFormVisible = !this.pollFormVisible
|
||||
this.newStatus.hasPoll = !this.newStatus.hasPoll
|
||||
},
|
||||
setPoll (poll) {
|
||||
this.newStatus.poll = poll
|
||||
|
|
@ -635,6 +759,84 @@ const PostStatusForm = {
|
|||
},
|
||||
propsToNative (props) {
|
||||
return propsToNative(props)
|
||||
},
|
||||
saveDraft () {
|
||||
if (!this.disableDraft &&
|
||||
!this.saveInhibited) {
|
||||
if (this.safeToSaveDraft) {
|
||||
return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus })
|
||||
.then(id => {
|
||||
if (this.newStatus.id !== id) {
|
||||
this.newStatus.id = id
|
||||
}
|
||||
this.saveable = false
|
||||
if (!this.shouldAutoSaveDraft) {
|
||||
this.clearStatus()
|
||||
this.$emit('draft-done')
|
||||
}
|
||||
})
|
||||
} else if (this.newStatus.id) {
|
||||
// There is a draft, but there is nothing in it, clear it
|
||||
return this.abandonDraft()
|
||||
.then(() => {
|
||||
this.saveable = false
|
||||
if (!this.shouldAutoSaveDraft) {
|
||||
this.clearStatus()
|
||||
this.$emit('draft-done')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
maybeAutoSaveDraft () {
|
||||
if (this.shouldAutoSaveDraft) {
|
||||
this.saveDraft(false)
|
||||
}
|
||||
},
|
||||
abandonDraft () {
|
||||
return this.$store.dispatch('abandonDraft', { id: this.newStatus.id })
|
||||
},
|
||||
getDraft (statusType, refId) {
|
||||
const maybeDraft = this.$store.state.drafts.drafts[this.draftId]
|
||||
if (this.draftId && maybeDraft) {
|
||||
return maybeDraft
|
||||
} else {
|
||||
const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId)
|
||||
|
||||
if (existingDrafts.length) {
|
||||
return existingDrafts[0]
|
||||
}
|
||||
}
|
||||
// No draft available, fall back
|
||||
},
|
||||
requestClose () {
|
||||
if (!this.saveable) {
|
||||
this.$emit('can-close')
|
||||
} else {
|
||||
this.$refs.draftCloser.requestClose()
|
||||
}
|
||||
},
|
||||
saveAndCloseDraft () {
|
||||
this.saveDraft().then(() => {
|
||||
this.$emit('can-close')
|
||||
})
|
||||
},
|
||||
discardAndCloseDraft () {
|
||||
this.abandonDraft().then(() => {
|
||||
this.$emit('can-close')
|
||||
})
|
||||
},
|
||||
addBeforeUnloadListener () {
|
||||
this._beforeUnloadListener ||= () => {
|
||||
this.saveDraft()
|
||||
}
|
||||
window.addEventListener('beforeunload', this._beforeUnloadListener)
|
||||
},
|
||||
removeBeforeUnloadListener () {
|
||||
if (this._beforeUnloadListener) {
|
||||
window.removeEventListener('beforeunload', this._beforeUnloadListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
258
src/components/post_status_form/post_status_form.scss
Normal file
258
src/components/post_status_form/post_status_form.scss
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
.post-status-form {
|
||||
position: relative;
|
||||
|
||||
.attachments {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.more-post-actions {
|
||||
height: 100%;
|
||||
|
||||
.btn {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5em;
|
||||
height: 2.5em;
|
||||
|
||||
.post-button-group {
|
||||
width: 10em;
|
||||
display: flex;
|
||||
|
||||
.post-button {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.more-post-actions {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.35em;
|
||||
padding: 0.35em;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.form-bottom-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-right: 7px;
|
||||
margin-right: 7px;
|
||||
max-width: 10em;
|
||||
}
|
||||
|
||||
.preview-heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preview-toggle {
|
||||
flex: 10 0 auto;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding-left: 0.5em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
svg,
|
||||
i {
|
||||
margin-left: 0.2em;
|
||||
font-size: 0.8em;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
font-style: italic;
|
||||
color: var(--textFaint);
|
||||
}
|
||||
|
||||
.preview-status {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reply-or-quote-selector {
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 0.5em;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.text-format {
|
||||
.only-format {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visibility-notice.edit-warning {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Order is not necessary but a good indicator
|
||||
.media-upload-icon {
|
||||
order: 1;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
order: 2;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.poll-icon {
|
||||
order: 3;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.media-upload-icon,
|
||||
.poll-icon,
|
||||
.emoji-icon {
|
||||
font-size: 1.85em;
|
||||
line-height: 1.1;
|
||||
flex: 1;
|
||||
padding: 0 0.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-upload-wrapper {
|
||||
margin-right: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
width: 18em;
|
||||
|
||||
img,
|
||||
video {
|
||||
object-fit: contain;
|
||||
max-height: 10em;
|
||||
}
|
||||
|
||||
.video {
|
||||
max-height: 10em;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.status-input-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.6em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.25em 0.5em 0.5em;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.input.form-post-body {
|
||||
// TODO: make a resizable textarea component?
|
||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||
overflow: hidden;
|
||||
transition: min-height 200ms 100ms;
|
||||
// stock padding + 1 line of text (for counter)
|
||||
padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
|
||||
// two lines of text
|
||||
height: calc(var(--post-line-height) * 1em);
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
background: transparent;
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.main-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0 0.5em;
|
||||
|
||||
&.error {
|
||||
color: var(--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 0.6; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.drop-indicator {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
border-radius: var(--roundness);
|
||||
border: 2px dashed var(--text);
|
||||
}
|
||||
|
||||
.auto-save-status {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,36 @@
|
|||
icon="circle-notch"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="quotable"
|
||||
role="radiogroup"
|
||||
class="btn-group reply-or-quote-selector"
|
||||
>
|
||||
<button
|
||||
:id="`reply-or-quote-option-${randomSeed}-reply`"
|
||||
class="btn button-default reply-or-quote-option"
|
||||
:class="{ toggled: !newStatus.quoting }"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
|
||||
:aria-checked="!newStatus.quoting"
|
||||
@click="newStatus.quoting = false"
|
||||
>
|
||||
{{ $t('post_status.reply_option') }}
|
||||
</button>
|
||||
<button
|
||||
:id="`reply-or-quote-option-${randomSeed}-quote`"
|
||||
class="btn button-default reply-or-quote-option"
|
||||
:class="{ toggled: newStatus.quoting }"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
|
||||
:aria-checked="newStatus.quoting"
|
||||
@click="newStatus.quoting = true"
|
||||
>
|
||||
{{ $t('post_status.quote_option') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showPreview"
|
||||
|
|
@ -131,7 +161,7 @@
|
|||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
class="form-control"
|
||||
class="input form-control"
|
||||
>
|
||||
<template #default="inputProps">
|
||||
<input
|
||||
|
|
@ -141,7 +171,7 @@
|
|||
:disabled="posting && !optimisticPosting"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
size="1"
|
||||
class="form-post-subject"
|
||||
class="input form-post-subject"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
|
|
@ -150,11 +180,11 @@
|
|||
v-model="newStatus.status"
|
||||
:suggest="emojiUserSuggestor"
|
||||
:placement="emojiPickerPlacement"
|
||||
class="form-control main-input"
|
||||
class="input form-control main-input"
|
||||
enable-sticker-picker
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:newline-on-ctrl-enter="submitOnEnter"
|
||||
enable-sticker-picker
|
||||
@input="onEmojiInputInput"
|
||||
@sticker-uploaded="addMediaFile"
|
||||
@sticker-upload-failed="uploadFailed"
|
||||
|
|
@ -168,7 +198,7 @@
|
|||
rows="1"
|
||||
cols="1"
|
||||
:disabled="posting && !optimisticPosting"
|
||||
class="form-post-body"
|
||||
class="input form-post-body"
|
||||
:class="{ 'scrollable-form': !!maxHeight }"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||
|
|
@ -205,9 +235,8 @@
|
|||
class="text-format"
|
||||
>
|
||||
<Select
|
||||
id="post-content-type"
|
||||
v-model="newStatus.contentType"
|
||||
class="form-control"
|
||||
class="input form-control"
|
||||
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
|
||||
>
|
||||
<option
|
||||
|
|
@ -233,8 +262,14 @@
|
|||
v-if="pollsAvailable"
|
||||
ref="pollForm"
|
||||
:visible="pollFormVisible"
|
||||
@update-poll="setPoll"
|
||||
:params="newStatus.poll"
|
||||
/>
|
||||
<span
|
||||
v-if="!disableDraft && shouldAutoSaveDraft"
|
||||
class="auto-save-status"
|
||||
>
|
||||
{{ autoSaveState }}
|
||||
</span>
|
||||
<div
|
||||
ref="bottom"
|
||||
class="form-bottom"
|
||||
|
|
@ -267,28 +302,58 @@
|
|||
<FAIcon icon="poll-h" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="posting"
|
||||
disabled
|
||||
class="btn button-default"
|
||||
>
|
||||
{{ $t('post_status.posting') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="isOverLengthLimit"
|
||||
disabled
|
||||
class="btn button-default"
|
||||
>
|
||||
{{ $t('post_status.post') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="uploadingFiles || disableSubmit"
|
||||
class="btn button-default"
|
||||
@click.stop.prevent="postStatus($event, newStatus)"
|
||||
>
|
||||
{{ $t('post_status.post') }}
|
||||
</button>
|
||||
<div class="btn-group post-button-group">
|
||||
<button
|
||||
class="btn button-default post-button"
|
||||
:disabled="isOverLengthLimit || posting || uploadingFiles || disableSubmit"
|
||||
@click.stop.prevent="postStatus($event, newStatus)"
|
||||
>
|
||||
<template v-if="posting">
|
||||
{{ $t('post_status.posting') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('post_status.post') }}
|
||||
</template>
|
||||
</button>
|
||||
<Popover
|
||||
v-if="!hideExtraActions"
|
||||
class="more-post-actions"
|
||||
:normal-button="true"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
<template #trigger>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="chevron-down"
|
||||
/>
|
||||
</template>
|
||||
<template #content="{close}">
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
v-if="!hideDraft || !disableDraft"
|
||||
class="menu-item dropdown-item"
|
||||
role="menu"
|
||||
:disabled="!safeToSaveDraft && saveable"
|
||||
:class="{ disabled: !safeToSaveDraft }"
|
||||
@click.prevent="saveDraft"
|
||||
@click="close"
|
||||
>
|
||||
<template v-if="closeable">
|
||||
{{ $t('post_status.save_to_drafts_and_close_button') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('post_status.save_to_drafts_button') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="showDropIcon !== 'hide'"
|
||||
|
|
@ -339,274 +404,14 @@
|
|||
</Checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<DraftCloser
|
||||
ref="draftCloser"
|
||||
@save="saveAndCloseDraft"
|
||||
@discard="discardAndCloseDraft"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./post_status_form.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.post-status-form {
|
||||
position: relative;
|
||||
|
||||
.attachments {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5em;
|
||||
height: 2.5em;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.35em;
|
||||
padding: 0.35em;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.form-bottom-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-right: 7px;
|
||||
margin-right: 7px;
|
||||
max-width: 10em;
|
||||
}
|
||||
|
||||
.preview-heading {
|
||||
display: flex;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.preview-toggle {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
svg,
|
||||
i {
|
||||
margin-left: 0.2em;
|
||||
font-size: 0.8em;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
font-style: italic;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
|
||||
.preview-status {
|
||||
border: 1px solid $fallback--border;
|
||||
border: 1px solid var(--border, $fallback--border);
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text-format {
|
||||
.only-format {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visibility-notice.edit-warning {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Order is not necessary but a good indicator
|
||||
.media-upload-icon {
|
||||
order: 1;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
order: 2;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.poll-icon {
|
||||
order: 3;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.media-upload-icon,
|
||||
.poll-icon,
|
||||
.emoji-icon {
|
||||
font-size: 1.85em;
|
||||
line-height: 1.1;
|
||||
flex: 1;
|
||||
padding: 0 0.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
// needs to be specific to override icon default color
|
||||
svg,
|
||||
i,
|
||||
label {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
svg,
|
||||
i {
|
||||
cursor: not-allowed;
|
||||
color: $fallback--icon;
|
||||
color: var(--btnDisabledText, $fallback--icon);
|
||||
|
||||
&:hover {
|
||||
color: $fallback--icon;
|
||||
color: var(--btnDisabledText, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-upload-wrapper {
|
||||
margin-right: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
width: 18em;
|
||||
|
||||
img,
|
||||
video {
|
||||
object-fit: contain;
|
||||
max-height: 10em;
|
||||
}
|
||||
|
||||
.video {
|
||||
max-height: 10em;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.status-input-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.6em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.25em 0.5em 0.5em;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.form-post-body {
|
||||
// TODO: make a resizable textarea component?
|
||||
box-sizing: content-box; // needed for easier computation of dynamic size
|
||||
overflow: hidden;
|
||||
transition: min-height 200ms 100ms;
|
||||
// stock padding + 1 line of text (for counter)
|
||||
padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
|
||||
// two lines of text
|
||||
height: calc(var(--post-line-height) * 1em);
|
||||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.main-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0 0.5em;
|
||||
|
||||
&.error {
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 0.6; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.drop-indicator {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
border: 2px dashed $fallback--text;
|
||||
border: 2px dashed var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="./post_status_form.scss" lang="scss"></style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue