{{#if realm === 'home'}} <h1 class="sr-only">Compose toot</h1> {{/if}} <div class="{{computedClassName}} {{hideAndFadeIn}}"> <ComposeAuthor /> {{#if contentWarningShown}} <div class="compose-content-warning-wrapper" transition:slide="{duration: 333}"> <ComposeContentWarning :realm :contentWarning /> </div> {{/if}} <ComposeInput :realm :text :autoFocus on:postAction="doPostStatus()" /> <ComposeLengthGauge :length :overLimit /> <ComposeToolbar :realm :postPrivacy :media :contentWarningShown :text /> <ComposeLengthIndicator :length :overLimit /> <ComposeMedia :realm :media :mediaDescriptions /> </div> <div class="compose-box-button-sentinel {{hideAndFadeIn}}" ref:sentinel></div> <div class="compose-box-button-wrapper {{realm === 'home' ? 'compose-button-sticky' : ''}} {{hideAndFadeIn}}" > <ComposeButton :length :overLimit :sticky on:click="onClickPostButton()" /> </div> {{#if !hideBottomBorder}} <div class="compose-box-border-bottom {{hideAndFadeIn}}"></div> {{/if}} <style> .compose-box { border-radius: 4px; padding: 20px 20px 0 20px; display: grid; align-items: flex-start; grid-template-areas: "avatar name handle handle" "avatar cw cw cw" "avatar input input input" "avatar gauge gauge gauge" "avatar toolbar toolbar length" "avatar media media media"; grid-template-columns: min-content minmax(0, max-content) 1fr 1fr; width: 560px; max-width: calc(100vw - 40px); } .compose-box.direct-reply { background-color: var(--status-direct-background); } .compose-box.slim-size { width: 540px; max-width: calc(100vw - 60px); } .compose-box-fade-in { transition: opacity 0.2s linear; /* main page reveal */ } .compose-box-border-bottom { height: 1px; background: var(--main-border); width: 100%; } .compose-box-button-wrapper { /* * We want pointer-events only for the sticky button, so use fit-content so that * the element doesn't take up the full width, and then set its left margin to * auto so that it sticks to the right. fit-content doesn't work in Edge, but * that just means that content that is level with the button is not clickable. */ width: -moz-fit-content; width: fit-content; margin-left: auto; display: flex; justify-content: flex-end; } .compose-box-button-wrapper.compose-button-sticky { position: -webkit-sticky; position: sticky; top: 10px; z-index: 5000; } .compose-content-warning-wrapper { grid-area: cw; } @media (max-width: 767px) { .compose-box { padding: 10px 10px 0 10px; max-width: calc(100vw - 20px); width: 580px; } .compose-box.slim-size { width: 560px; max-width: calc(100vw - 40px); } .compose-box-button-wrapper.compose-button-sticky { top: 5px; } } </style> <script> import ComposeToolbar from './ComposeToolbar.html' import ComposeLengthGauge from './ComposeLengthGauge.html' import ComposeLengthIndicator from './ComposeLengthIndicator.html' import ComposeAuthor from './ComposeAuthor.html' import ComposeInput from './ComposeInput.html' import ComposeButton from './ComposeButton.html' import ComposeMedia from './ComposeMedia.html' import ComposeContentWarning from './ComposeContentWarning.html' import { measureText } from '../../_utils/measureText' import { CHAR_LIMIT, POST_PRIVACY_OPTIONS } from '../../_static/statuses' import { store } from '../../_store/store' import { slide } from 'svelte-transitions' import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose' import { importDialogs } from '../../_utils/asyncModules' import { classname } from '../../_utils/classname' export default { oncreate() { let realm = this.get('realm') if (realm === 'home') { this.setupStickyObserver() } else if (realm !== 'dialog') { // if this is a reply, populate the handle immediately insertHandleForReply(realm) } let replySpoiler = this.get('replySpoiler') if (replySpoiler) { // default spoiler is same as the replied-to status setReplySpoiler(realm, replySpoiler) } let replyVisibility = this.get('replyVisibility') if (replyVisibility) { // make sure the visibility is consistent with the replied-to status setReplyVisibility(realm, replyVisibility) } }, ondestroy() { this.teardownStickyObserver() }, components: { ComposeAuthor, ComposeToolbar, ComposeLengthGauge, ComposeLengthIndicator, ComposeInput, ComposeButton, ComposeMedia, ComposeContentWarning }, store: () => store, computed: { computedClassName: (overLimit, realm, size, postPrivacyKey, isReply) => { return classname( 'compose-box', overLimit && 'over-char-limit', size === 'slim' && 'slim-size', isReply && postPrivacyKey === 'direct' && 'direct-reply' ) }, hideAndFadeIn: (hidden) => { return classname( 'compose-box-fade-in', hidden && 'hidden' ) }, composeData: ($currentComposeData, realm) => $currentComposeData[realm] || {}, text: (composeData) => composeData.text || '', media: (composeData) => composeData.media || [], postPrivacy: (postPrivacyKey) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey), defaultPostPrivacyKey: ($currentVerifyCredentials) => $currentVerifyCredentials.source.privacy, postPrivacyKey: (composeData, defaultPostPrivacyKey) => composeData.postPrivacy || defaultPostPrivacyKey, textLength: (text) => measureText(text), contentWarningLength: (contentWarning) => measureText(contentWarning), length: (textLength, contentWarningLength, contentWarningShown) => { return textLength + (contentWarningShown ? contentWarningLength : 0) }, overLimit: (length) => length > CHAR_LIMIT, contentWarningShown: (composeData) => composeData.contentWarningShown, contentWarning: (composeData) => composeData.contentWarning || '', timelineInitialized: ($timelineInitialized) => $timelineInitialized, mediaDescriptions: (composeData) => composeData.mediaDescriptions || [] }, transitions: { slide }, methods: { async onClickPostButton() { if (this.get('sticky')) { // when the button is sticky, we're scrolled down the home timeline, // so we should launch a new compose dialog let dialogs = await importDialogs() dialogs.showComposeDialog() } else { // else we're actually posting a new toot this.doPostStatus(); } }, doPostStatus() { let text = this.get('text') let media = this.get('media') let postPrivacyKey = this.get('postPrivacyKey') let contentWarning = this.get('contentWarning') let sensitive = media.length && !!contentWarning let realm = this.get('realm') let mediaIds = media.map(_ => _.data.id) let inReplyTo = (realm === 'home' || realm === 'dialog') ? null : realm let overLimit = this.get('overLimit') let mediaDescriptions = this.get('mediaDescriptions') let inReplyToUuid = this.get('inReplyToUuid') if (!text || overLimit) { return // do nothing if invalid } /* no await */ postStatus(realm, text, inReplyTo, mediaIds, sensitive, contentWarning, postPrivacyKey, mediaDescriptions, inReplyToUuid) }, setupStickyObserver() { this.__stickyObserver = new IntersectionObserver(entries => { this.set({sticky: !entries[0].isIntersecting}) }) this.__stickyObserver.observe(this.refs.sentinel) // also create a one-shot observer for the $timelineInitialized event, // due to a bug in Firefox where when the scrollTop is set // manually, the other observer doesn't necessarily fire this.observe('timelineInitialized', timelineInitialized => { if (timelineInitialized) { let observer = new IntersectionObserver(entries => { this.set({sticky: !entries[0].isIntersecting}) observer.disconnect() }) observer.observe(this.refs.sentinel) } }, {init: false}) }, teardownStickyObserver() { if (this.__stickyObserver) { this.__stickyObserver.disconnect() } } } } </script>