Merge remote-tracking branch 'origin/develop' into migrate/vuex-to-pinia

This commit is contained in:
Henry Jameson 2025-01-30 18:08:05 +02:00
commit 58e18d48df
489 changed files with 31167 additions and 9871 deletions

View file

@ -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)
}
}
}
}

View 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;
}
}

View file

@ -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>