259 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| {#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 { importShowComposeDialog } from '../dialog/asyncDialogs'
 | |
|   import { classname } from '../../_utils/classname'
 | |
|   import { observe } from 'svelte-extras'
 | |
| 
 | |
|   export default {
 | |
|     oncreate () {
 | |
|       let { realm } = this.get()
 | |
|       if (realm === 'home') {
 | |
|         this.setupStickyObserver()
 | |
|       } else if (realm !== 'dialog') {
 | |
|         // if this is a reply, populate the handle immediately
 | |
|         insertHandleForReply(realm)
 | |
|       }
 | |
| 
 | |
|       let { replySpoiler } = this.get()
 | |
|       if (replySpoiler) {
 | |
|         // default spoiler is same as the replied-to status
 | |
|         setReplySpoiler(realm, replySpoiler)
 | |
|       }
 | |
| 
 | |
|       let { replyVisibility } = this.get()
 | |
|       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
 | |
|     },
 | |
|     data: () => ({
 | |
|       size: void 0,
 | |
|       isReply: false,
 | |
|       autoFocus: false,
 | |
|       sticky: false,
 | |
|       hideBottomBorder: false,
 | |
|       hidden: false
 | |
|     }),
 | |
|     store: () => store,
 | |
|     computed: {
 | |
|       computedClassName: ({ overLimit, realm, size, postPrivacyKey, isReply }) => (classname(
 | |
|         'compose-box',
 | |
|         overLimit && 'over-char-limit',
 | |
|         size === 'slim' && 'slim-size',
 | |
|         isReply && postPrivacyKey === 'direct' && 'direct-reply'
 | |
|       )),
 | |
|       hideAndFadeIn: ({ hidden }) => (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 }) => (
 | |
|         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: {
 | |
|       observe,
 | |
|       async onClickPostButton () {
 | |
|         let { sticky } = this.get()
 | |
|         if (sticky) {
 | |
|           // when the button is sticky, we're scrolled down the home timeline,
 | |
|           // so we should launch a new compose dialog
 | |
|           let showComposeDialog = await importShowComposeDialog()
 | |
|           showComposeDialog()
 | |
|         } else {
 | |
|           // else we're actually posting a new toot
 | |
|           this.doPostStatus()
 | |
|         }
 | |
|       },
 | |
|       doPostStatus () {
 | |
|         let {
 | |
|           text,
 | |
|           media,
 | |
|           postPrivacyKey,
 | |
|           contentWarning,
 | |
|           realm,
 | |
|           overLimit,
 | |
|           mediaDescriptions,
 | |
|           inReplyToUuid
 | |
|         } = this.get()
 | |
|         let sensitive = media.length && !!contentWarning
 | |
|         let mediaIds = media.map(_ => _.data.id)
 | |
|         let inReplyTo = (realm === 'home' || realm === 'dialog') ? null : realm
 | |
| 
 | |
|         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>
 |