semaphore/src/routes/_components/compose/ComposeBox.html

256 lines
9.0 KiB
HTML

{#if realm === 'home'}
<h2 class="sr-only">{intl.composeStatus}</h2>
{/if}
<ComposeFileDrop {realm} >
<div class="{computedClassName} {hideAndFadeIn}">
<ComposeAuthor {realm} {dialogId} />
{#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} />
<ComposeAutosuggest {realm} {text} {dialogId} />
{#if poll && poll.options && poll.options.length}
<div class="compose-poll-wrapper"
transition:slide="{duration: 333}">
<ComposePoll {realm} {poll} />
</div>
{/if}
<ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} {poll} />
<ComposeLengthIndicator {length} {overLimit} />
<ComposeMedia {realm} {media} />
<ComposeMediaSensitive {realm} {media} {sensitive} {contentWarning} {contentWarningShown} />
</div>
</ComposeFileDrop>
<ComposeStickyButton {showSticky}
{overLimit}
{hideAndFadeIn}
on:postAction="doPostStatus()" />
{#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 autosuggest autosuggest autosuggest"
"avatar poll poll poll"
"avatar toolbar toolbar length"
"avatar media media media"
"avatar sensitive sensitive sensitive";
grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
position: relative;
}
.compose-box.direct-reply {
background-color: var(--status-direct-background);
}
:global(.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-content-warning-wrapper {
grid-area: cw;
}
.compose-poll-wrapper {
grid-area: poll;
}
@media (max-width: 767px) {
.compose-box {
padding: 10px 10px 0 10px;
}
.compose-box-realm-dialog {
overflow-x: hidden;
}
}
@media (max-width: 240px) {
.compose-box {
padding: 10px 5px 0 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 ComposeStickyButton from './ComposeStickyButton.html'
import ComposeMedia from './ComposeMedia.html'
import ComposeContentWarning from './ComposeContentWarning.html'
import ComposeFileDrop from './ComposeFileDrop.html'
import ComposeAutosuggest from './ComposeAutosuggest.html'
import ComposePoll from './ComposePoll.html'
import ComposeMediaSensitive from './ComposeMediaSensitive.html'
import { measureText } from '../../_utils/measureText.js'
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses.js'
import { store } from '../../_store/store.js'
import { slide } from '../../_transitions/slide.js'
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose.js'
import { classname } from '../../_utils/classname.js'
import { POLL_EXPIRY_DEFAULT } from '../../_static/polls.js'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask.js'
export default {
oncreate () {
const { realm, replySpoiler, replyVisibility } = this.get()
if (realm !== 'home' && realm !== 'dialog') {
// if this is a reply, populate the handle immediately
/* no await */ insertHandleForReply(realm)
}
if (replySpoiler) {
// default spoiler is same as the replied-to status
setReplySpoiler(realm, replySpoiler)
}
if (replyVisibility) {
// make sure the visibility is consistent with the replied-to status
setReplyVisibility(realm, replyVisibility)
}
},
components: {
ComposeAuthor,
ComposeToolbar,
ComposeLengthGauge,
ComposeLengthIndicator,
ComposeInput,
ComposeStickyButton,
ComposeMedia,
ComposeContentWarning,
ComposeFileDrop,
ComposeAutosuggest,
ComposePoll,
ComposeMediaSensitive
},
data: () => ({
size: undefined,
isReply: false,
autoFocus: false,
hideBottomBorder: false,
hidden: false,
dialogId: undefined,
aboutToPostStatus: false
}),
store: () => store,
computed: {
computedClassName: ({ overLimit, realm, size, postPrivacyKey, isReply }) => (classname(
'compose-box',
`compose-box-realm-${realm}`,
overLimit && 'over-char-limit',
isReply && postPrivacyKey === 'direct' && 'direct-reply'
)),
hideAndFadeIn: ({ hidden }) => (classname(
'compose-box-fade-in',
hidden && 'hidden'
)),
showSticky: ({ realm }) => realm === 'home',
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
text: ({ composeData }) => composeData.text || '',
media: ({ composeData }) => composeData.media || [],
poll: ({ composeData }) => composeData.poll,
inReplyToId: ({ composeData }) => composeData.inReplyToId,
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
($currentVerifyCredentials && $currentVerifyCredentials.source.privacy) || 'public'
),
postPrivacyKey: ({ composeData, defaultPostPrivacyKey }) => composeData.postPrivacy || defaultPostPrivacyKey,
textLength: ({ text }) => measureText(text),
contentWarningLength: ({ contentWarning }) => measureText(contentWarning),
length: ({ textLength, contentWarningLength, contentWarningShown }) => (
textLength + (contentWarningShown ? contentWarningLength : 0)
),
overLimit: ({ length, $maxStatusChars }) => length > $maxStatusChars,
contentWarningShown: ({ composeData }) => composeData.contentWarningShown,
contentWarning: ({ composeData }) => composeData.contentWarning || '',
sensitive: ({ composeData }) => !!composeData.sensitive
},
transitions: {
slide
},
methods: {
async doPostStatus () {
const { aboutToPostStatus } = this.get()
const { postingStatus } = this.store.get()
if (aboutToPostStatus || postingStatus) { // do nothing if the user rapidly taps the Ctrl-Enter key
console.log('ignored post command', { aboutToPostStatus, postingStatus })
return
}
// The reason we add a scheduleIdleTask delay here is because we also use scheduleIdleTask
// in ComposeInput.html to debounce the input events. If the user is very fast at typing
// at their keyboard and quickly presses Ctrl+Enter or the "Toot" button then there could
// be a race condition where not all of their status is posted.
this.set({ aboutToPostStatus: true })
scheduleIdleTask(() => {
this.set({ aboutToPostStatus: false })
this.doPostStatusAfterDelay()
})
},
doPostStatusAfterDelay () {
const {
text,
media,
postPrivacyKey,
contentWarning,
realm,
overLimit,
inReplyToUuid, // typical replies, using Semaphore-specific uuid
inReplyToId, // delete-and-redraft replies, using standard id
poll,
sensitive
} = this.get()
const mediaIds = media.map(_ => _.data.id)
const mediaDescriptions = media.map(_ => _.description)
const mediaFocalPoints = media.map(_ => [_.focusX, _.focusY])
const inReplyTo = inReplyToId || ((realm === 'home' || realm === 'dialog') ? null : realm)
if (overLimit || (!text && !media.length)) {
return // do nothing if invalid
}
const hasPoll = poll && poll.options && poll.options.length
if (hasPoll) {
// validate poll
if (poll.options.length < 2 || !poll.options.every(Boolean)) {
return
}
}
// convert internal poll format to the format Mastodon's REST API uses
const pollToPost = hasPoll && {
expires_in: (poll.expiry || POLL_EXPIRY_DEFAULT).toString(),
multiple: !!poll.multiple,
options: poll.options
}
/* no await */ postStatus(realm, text, inReplyTo, mediaIds,
sensitive, contentWarning, postPrivacyKey,
mediaDescriptions, inReplyToUuid, pollToPost,
mediaFocalPoints)
}
}
}
</script>