feat: add threaded drafts & posts (#2715)
Co-authored-by: Sebastian Di Luzio <sebastian.di-luzio@iu.org> Co-authored-by: Emanuel Pina <contacto@emanuelpina.pt> Co-authored-by: lazzzis <lazzzis@outlook.com> Co-authored-by: Joaquín Sánchez <userquin@gmail.com> Co-authored-by: TAKAHASHI Shuuji <shuuji3@gmail.com> Co-authored-by: Francesco <129339155+katullo11@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: patak-dev <matias.capeletto@gmail.com>
This commit is contained in:
parent
0538f97ada
commit
1234fb2dd1
17 changed files with 634 additions and 377 deletions
|
@ -67,7 +67,7 @@ function handleFavouritedBoostedByClose() {
|
|||
@close="handlePublishClose"
|
||||
>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<PublishWidget
|
||||
<PublishWidgetList
|
||||
v-if="dialogDraftKey"
|
||||
:draft-key="dialogDraftKey" expanded flex-1 w-0
|
||||
@published="handlePublished"
|
||||
|
|
45
components/publish/PublishThreadTools.vue
Normal file
45
components/publish/PublishThreadTools.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
draftKey: string
|
||||
draftItemIndex: number
|
||||
}>()
|
||||
|
||||
const { threadIsActive, addThreadItem, threadItems, removeThreadItem } = useThreadComposer(props.draftKey)
|
||||
|
||||
const isRemovableItem = computed(() => threadIsActive.value && props.draftItemIndex < threadItems.value.length - 1)
|
||||
|
||||
function addOrRemoveItem() {
|
||||
if (isRemovableItem.value)
|
||||
removeThreadItem(props.draftItemIndex)
|
||||
|
||||
else
|
||||
addThreadItem()
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const label = computed(() => {
|
||||
if (!isRemovableItem.value && props.draftItemIndex === 0)
|
||||
return t('tooltip.start_thread')
|
||||
|
||||
return isRemovableItem.value ? t('tooltip.remove_thread_item') : t('tooltip.add_thread_item')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-row rounded-3 :class="{ 'bg-border': threadIsActive }">
|
||||
<div
|
||||
v-if="threadIsActive" dir="ltr" pointer-events-none pe-1 pt-2 pl-2 text-sm tabular-nums text-secondary flex
|
||||
gap="0.5"
|
||||
>
|
||||
{{ draftItemIndex + 1 }}<span text-secondary-light>/</span><span text-secondary-light>{{ threadItems.length
|
||||
}}</span>
|
||||
</div>
|
||||
<CommonTooltip placement="top" :content="label">
|
||||
<button btn-action-icon :aria-label="label" @click="addOrRemoveItem">
|
||||
<div v-if="isRemovableItem" i-ri:chat-delete-line />
|
||||
<div v-else i-ri:chat-new-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</template>
|
|
@ -2,17 +2,19 @@
|
|||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import stringLength from 'string-length'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { Draft } from '~/types'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const {
|
||||
draftKey,
|
||||
initial = getDefaultDraft,
|
||||
draftItemIndex,
|
||||
expanded = false,
|
||||
placeholder,
|
||||
dialogLabelledBy,
|
||||
initial = getDefaultDraftItem,
|
||||
} = defineProps<{
|
||||
draftKey?: string
|
||||
initial?: () => Draft
|
||||
draftKey: string
|
||||
draftItemIndex: number
|
||||
initial?: () => DraftItem
|
||||
placeholder?: string
|
||||
inReplyToId?: string
|
||||
inReplyToVisibility?: mastodon.v1.StatusVisibility
|
||||
|
@ -26,8 +28,17 @@ const emit = defineEmits<{
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
const draftState = useDraft(draftKey, initial)
|
||||
const { draft } = draftState
|
||||
const { threadItems, threadIsActive, publishThread } = useThreadComposer(draftKey)
|
||||
|
||||
const draft = computed({
|
||||
get: () => threadItems.value[draftItemIndex],
|
||||
set: (updatedDraft: DraftItem) => {
|
||||
threadItems.value[draftItemIndex] = updatedDraft
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const isFinalItemOfThread = computed(() => draftItemIndex === threadItems.value.length - 1)
|
||||
|
||||
const {
|
||||
isExceedingAttachmentLimit,
|
||||
|
@ -43,8 +54,8 @@ const {
|
|||
|
||||
const { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage, publishSpoilerText } = usePublish(
|
||||
{
|
||||
draftState,
|
||||
...{ expanded: toRef(() => expanded), isUploading, initialDraft: toRef(() => initial) },
|
||||
draftItem: draft,
|
||||
...{ expanded: toRef(() => expanded), isUploading, initialDraft: initial, isPartOfThread: false },
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -181,9 +192,13 @@ async function toggleSensitive() {
|
|||
}
|
||||
|
||||
async function publish() {
|
||||
const status = await publishDraft()
|
||||
if (status)
|
||||
emit('published', status)
|
||||
const publishResult = await (threadIsActive.value ? publishThread() : publishDraft())
|
||||
if (publishResult) {
|
||||
if (Array.isArray(publishResult))
|
||||
failedMessages.value = publishResult
|
||||
else
|
||||
emit('published', publishResult)
|
||||
}
|
||||
}
|
||||
|
||||
useWebShareTarget(async ({ data: { data, action } }: any) => {
|
||||
|
@ -215,10 +230,6 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
|
|||
if (e.key === '?')
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -228,286 +239,304 @@ onDeactivated(() => {
|
|||
{{ $t('state.editing') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div flex gap-3 flex-1>
|
||||
<NuxtLink self-start :to="getAccountRoute(currentUser.account)">
|
||||
<AccountBigAvatar :account="currentUser.account" square />
|
||||
</NuxtLink>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
flex w-0 flex-col gap-3 flex-1
|
||||
border="2 dashed transparent"
|
||||
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
|
||||
>
|
||||
<ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying>
|
||||
<button v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red @click="draft.mentions?.splice(i, 1)">
|
||||
{{ accountToShortHandle(m) }}
|
||||
</button>
|
||||
</ContentMentionGroup>
|
||||
|
||||
<div v-if="draft.params.sensitive">
|
||||
<input
|
||||
v-model="publishSpoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('placeholder.content_warning')"
|
||||
p2 border-rounded w-full bg-transparent
|
||||
outline-none border="~ base"
|
||||
>
|
||||
</div>
|
||||
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<header id="publish-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.publish_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_publish_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('action.clear_publish_failed')"
|
||||
@click="failedMessages = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div relative flex-1 flex flex-col min-h-30>
|
||||
<EditorContent
|
||||
:editor="editor"
|
||||
flex max-w-full
|
||||
:class="{
|
||||
'md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain': shouldExpanded,
|
||||
'py2 px3.5 bg-dm rounded-4 me--1 ms--1 mt--1': isDM,
|
||||
}"
|
||||
@keydown="stopQuestionMarkPropagation"
|
||||
@keydown.esc.prevent="editor?.commands.blur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-fill />
|
||||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<CommonErrorMessage
|
||||
v-else-if="failedAttachments.length > 0"
|
||||
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
|
||||
>
|
||||
<header id="upload-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.upload_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_upload_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_upload_failed')" @click="failedAttachments = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
|
||||
{{ $t('state.attachments_exceed_server_limit') }}
|
||||
</div>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="error in failedAttachments" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ error[1] }}:</strong>
|
||||
<span>{{ error[0] }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
|
||||
<PublishAttachment
|
||||
v-for="(att, idx) in draft.attachments" :key="att.id"
|
||||
:attachment="att"
|
||||
:dialog-labelled-by="dialogLabelledBy ?? (draft.editingStatus ? 'state-editing' : undefined)"
|
||||
@remove="removeAttachment(idx)"
|
||||
@set-description="setDescription(att, $event)"
|
||||
/>
|
||||
<div>
|
||||
<NuxtLink self-start :to="getAccountRoute(currentUser.account)">
|
||||
<AccountBigAvatar :account="currentUser.account" square />
|
||||
</NuxtLink>
|
||||
<div v-if="!isFinalItemOfThread" w-full h-full flex mt--3px justify-center>
|
||||
<div w-1px border="x base" mb-6 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div flex gap-4>
|
||||
<div w-12 h-full sm:block hidden />
|
||||
<div flex="~ col 1" max-w-full>
|
||||
<form v-if="isExpanded && draft.params.poll" my-4 flex="~ 1 col" gap-3 m="s--1">
|
||||
|
||||
<div w-full>
|
||||
<div flex gap-3 flex-1>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<div
|
||||
v-for="(option, index) in draft.params.poll.options"
|
||||
:key="index"
|
||||
flex="~ row"
|
||||
gap-3
|
||||
ref="dropZoneRef" flex w-0 flex-col gap-3 flex-1 border="2 dashed transparent"
|
||||
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
|
||||
>
|
||||
<input
|
||||
:value="option"
|
||||
bg-base
|
||||
border="~ base" flex-1 h10 pe-4 rounded-2 w-full flex="~ row"
|
||||
items-center relative focus-within:box-shadow-outline gap-3
|
||||
px-4 py-2
|
||||
:placeholder="$t('polls.option_placeholder', { current: index + 1, max: currentInstance?.configuration?.polls.maxOptions })"
|
||||
class="option-input"
|
||||
@input="editPollOptionDraft($event, index)"
|
||||
>
|
||||
<CommonTooltip placement="top" :content="$t('polls.remove_option')" class="delete-button">
|
||||
<ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying>
|
||||
<button
|
||||
btn-action-icon class="hover:bg-red/75"
|
||||
:disabled="index === draft.params.poll!.options.length - 1 && (index + 1 !== currentInstance?.configuration?.polls.maxOptions || draft.params.poll!.options[index].length === 0)"
|
||||
@click.prevent="deletePollOption(index)"
|
||||
v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red
|
||||
@click="draft.mentions?.splice(i, 1)"
|
||||
>
|
||||
<div i-ri:delete-bin-line />
|
||||
{{ accountToShortHandle(m) }}
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<span
|
||||
v-if="currentInstance?.configuration?.polls.maxCharactersPerOption"
|
||||
class="char-limit-radial"
|
||||
aspect-ratio-1
|
||||
h-10
|
||||
:style="{ background: `radial-gradient(closest-side, rgba(var(--rgb-bg-base)) 79%, transparent 80% 100%), conic-gradient(${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption > 1 ? 'var(--c-danger)' : 'var(--c-primary)'} ${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption * 100}%, var(--c-primary-fade) 0)` }"
|
||||
>{{ draft.params.poll!.options[index].length }}</span>
|
||||
</ContentMentionGroup>
|
||||
|
||||
<div v-if="draft.params.sensitive">
|
||||
<input
|
||||
v-model="publishSpoilerText" type="text" :placeholder="$t('placeholder.content_warning')" p2
|
||||
border-rounded w-full bg-transparent outline-none border="~ base"
|
||||
>
|
||||
</div>
|
||||
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<header id="publish-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.publish_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_publish_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_publish_failed')" @click="failedMessages = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div relative flex-1 flex flex-col min-h-30>
|
||||
<EditorContent
|
||||
:editor="editor" flex max-w-full
|
||||
:class="{
|
||||
'md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain': shouldExpanded,
|
||||
'py2 px3.5 bg-dm rounded-4 me--1 ms--1 mt--1': isDM,
|
||||
}"
|
||||
@keydown="stopQuestionMarkPropagation"
|
||||
@keydown.esc.prevent="editor?.commands.blur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-fill />
|
||||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<CommonErrorMessage
|
||||
v-else-if="failedAttachments.length > 0"
|
||||
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
|
||||
>
|
||||
<header id="upload-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.upload_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_upload_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_upload_failed')" @click="failedAttachments = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
|
||||
{{ $t('state.attachments_exceed_server_limit') }}
|
||||
</div>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="error in failedAttachments" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ error[1] }}:</strong>
|
||||
<span>{{ error[0] }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
|
||||
<PublishAttachment
|
||||
v-for="(att, idx) in draft.attachments" :key="att.id" :attachment="att"
|
||||
:dialog-labelled-by="dialogLabelledBy ?? (draft.editingStatus ? 'state-editing' : undefined)"
|
||||
@remove="removeAttachment(idx)" @set-description="setDescription(att, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full
|
||||
border="t base"
|
||||
>
|
||||
<PublishEmojiPicker
|
||||
@select="insertEmoji"
|
||||
@select-custom="insertCustomEmoji"
|
||||
>
|
||||
<button btn-action-icon :title="$t('tooltip.emojis')" :aria-label="$t('tooltip.add_emojis')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
</div>
|
||||
|
||||
<CommonTooltip v-if="draft.params.poll === undefined" placement="top" :content="$t('tooltip.add_media')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="draft.attachments.length === 0">
|
||||
<CommonTooltip v-if="!draft.params.poll" placement="top" :content="$t('polls.create')">
|
||||
<button btn-action-icon :aria-label="$t('polls.create')" @click="draft.params.poll = { options: [''], expiresIn: expiresInOptions[expiresInDefaultOptionIndex].seconds }">
|
||||
<div i-ri:chat-poll-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div v-else rounded-full b-1 border-dark flex="~ row" gap-1>
|
||||
<CommonTooltip placement="top" :content="$t('polls.cancel')">
|
||||
<button btn-action-icon b-r border-dark :aria-label="$t('polls.cancel')" @click="draft.params.poll = undefined">
|
||||
<div i-ri:close-line />
|
||||
<div flex="~ col 1" max-w-full>
|
||||
<form v-if="isExpanded && draft.params.poll" my-4 flex="~ 1 col" gap-3 m="s--1">
|
||||
<div v-for="(option, index) in draft.params.poll.options" :key="index" flex="~ row" gap-3>
|
||||
<input
|
||||
:value="option" bg-base border="~ base" flex-1 h10 pe-4 rounded-2 w-full flex="~ row" items-center
|
||||
relative focus-within:box-shadow-outline gap-3 px-4 py-2
|
||||
:placeholder="$t('polls.option_placeholder', { current: index + 1, max: currentInstance?.configuration?.polls.maxOptions })"
|
||||
class="option-input" @input="editPollOptionDraft($event, index)"
|
||||
>
|
||||
<CommonTooltip placement="top" :content="$t('polls.remove_option')" class="delete-button">
|
||||
<button
|
||||
btn-action-icon class="hover:bg-red/75"
|
||||
:disabled="index === draft.params.poll!.options.length - 1 && (index + 1 !== currentInstance?.configuration?.polls.maxOptions || draft.params.poll!.options[index].length === 0)"
|
||||
@click.prevent="deletePollOption(index)"
|
||||
>
|
||||
<div i-ri:delete-bin-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<CommonTooltip placement="top" :content="$t('polls.settings')">
|
||||
<button :aria-label="$t('polls.settings')" btn-action-icon w-12>
|
||||
<div i-ri:list-settings-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<div flex="~ col" gap-1 p-2>
|
||||
<CommonCheckbox v-model="draft.params.poll.multiple" :label="draft.params.poll.multiple ? $t('polls.disallow_multiple') : $t('polls.allow_multiple')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:checkbox-multiple-blank-line" icon-unchecked="i-ri:checkbox-blank-circle-line" />
|
||||
<CommonCheckbox v-model="draft.params.poll.hideTotals" :label="draft.params.poll.hideTotals ? $t('polls.show_votes') : $t('polls.hide_votes')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:eye-close-line" icon-unchecked="i-ri:eye-line" />
|
||||
</div>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<CommonDropdown placement="bottom">
|
||||
<CommonTooltip placement="top" :content="$t('polls.expiration')">
|
||||
<button :aria-label="$t('polls.expiration')" btn-action-icon w-12>
|
||||
<div i-ri:hourglass-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="expiresInOption in expiresInOptions"
|
||||
:key="expiresInOption.seconds"
|
||||
:text="expiresInOption.label"
|
||||
:checked="draft.params.poll!.expiresIn === expiresInOption.seconds"
|
||||
@click="draft.params.poll!.expiresIn = expiresInOption.seconds"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<span
|
||||
v-if="currentInstance?.configuration?.polls.maxCharactersPerOption" class="char-limit-radial"
|
||||
aspect-ratio-1 h-10
|
||||
:style="{ background: `radial-gradient(closest-side, rgba(var(--rgb-bg-base)) 79%, transparent 80% 100%), conic-gradient(${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption > 1 ? 'var(--c-danger)' : 'var(--c-primary)'} ${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption * 100}%, var(--c-primary-fade) 0)` }"
|
||||
>{{
|
||||
draft.params.poll!.options[index].length }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
|
||||
<CommonDropdown placement="bottom" auto-boundary-max-size>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
|
||||
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
|
||||
<div v-else i-ri:translate-2 />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</form>
|
||||
<div v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full border="t base">
|
||||
<PublishEmojiPicker @select="insertEmoji" @select-custom="insertCustomEmoji">
|
||||
<button btn-action-icon :title="$t('tooltip.emojis')" :aria-label="$t('tooltip.add_emojis')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
|
||||
<template #popper>
|
||||
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
|
||||
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
|
||||
<div v-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
<CommonTooltip
|
||||
v-if="draft.params.poll === undefined" placement="top" :content="$t('tooltip.add_media')"
|
||||
>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="draft.attachments.length === 0">
|
||||
<CommonTooltip v-if="!draft.params.poll" placement="top" :content="$t('polls.create')">
|
||||
<button
|
||||
btn-action-icon :aria-label="$t('polls.create')"
|
||||
@click="draft.params.poll = { options: [''], expiresIn: expiresInOptions[expiresInDefaultOptionIndex].seconds }"
|
||||
>
|
||||
<div i-ri:chat-poll-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div v-else rounded-full b-1 border-dark flex="~ row" gap-1>
|
||||
<CommonTooltip placement="top" :content="$t('polls.cancel')">
|
||||
<button
|
||||
btn-action-icon b-r border-dark :aria-label="$t('polls.cancel')"
|
||||
@click="draft.params.poll = undefined"
|
||||
>
|
||||
<div i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<CommonTooltip placement="top" :content="$t('polls.settings')">
|
||||
<button :aria-label="$t('polls.settings')" btn-action-icon w-12>
|
||||
<div i-ri:list-settings-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<div flex="~ col" gap-1 p-2>
|
||||
<CommonCheckbox
|
||||
v-model="draft.params.poll.multiple"
|
||||
:label="draft.params.poll.multiple ? $t('polls.disallow_multiple') : $t('polls.allow_multiple')"
|
||||
px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full
|
||||
icon-checked="i-ri:checkbox-multiple-blank-line"
|
||||
icon-unchecked="i-ri:checkbox-blank-circle-line"
|
||||
/>
|
||||
<CommonCheckbox
|
||||
v-model="draft.params.poll.hideTotals"
|
||||
:label="draft.params.poll.hideTotals ? $t('polls.show_votes') : $t('polls.hide_votes')" px-2 gap-3
|
||||
h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:eye-close-line"
|
||||
icon-unchecked="i-ri:eye-line"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<CommonDropdown placement="bottom">
|
||||
<CommonTooltip placement="top" :content="$t('polls.expiration')">
|
||||
<button :aria-label="$t('polls.expiration')" btn-action-icon w-12>
|
||||
<div i-ri:hourglass-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="expiresInOption in expiresInOptions" :key="expiresInOption.seconds"
|
||||
:text="expiresInOption.label" :checked="draft.params.poll!.expiresIn === expiresInOption.seconds"
|
||||
@click="draft.params.poll!.expiresIn = expiresInOption.seconds"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<CommonTooltip v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top" :content="$t('tooltip.publish_failed')">
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<CommonTooltip v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')" :disabled="!(isPublishDisabled || isExceedingCharacterLimit)">
|
||||
<button
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center
|
||||
md:w-fit
|
||||
class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit"
|
||||
aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
|
||||
<CommonDropdown placement="bottom" auto-boundary-max-size>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
|
||||
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
|
||||
<div v-else i-ri:translate-2 />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
|
||||
<template #popper>
|
||||
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
|
||||
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
|
||||
<div v-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button
|
||||
:disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')"
|
||||
btn-action-icon :class="{ 'w-12': !draft.editingStatus }"
|
||||
>
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<PublishThreadTools :draft-item-index="draftItemIndex" :draft-key="draftKey" />
|
||||
|
||||
<CommonTooltip
|
||||
v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top"
|
||||
:content="$t('tooltip.publish_failed')"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit
|
||||
aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip
|
||||
v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')"
|
||||
:disabled="!(isPublishDisabled || isExceedingCharacterLimit)"
|
||||
>
|
||||
<button
|
||||
v-if="!threadIsActive || isFinalItemOfThread"
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit" aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<template v-if="threadIsActive">
|
||||
<span>{{ $t('action.publish_thread') }} </span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</template>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -515,27 +544,29 @@ onDeactivated(() => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.publish-button[aria-disabled=true] {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
.publish-button[aria-disabled=true]:hover {
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
.option-input:focus + .delete-button {
|
||||
display: none;
|
||||
}
|
||||
.publish-button[aria-disabled=true] {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
|
||||
.option-input:not(:focus) + .delete-button + .char-limit-radial {
|
||||
display: none;
|
||||
}
|
||||
.publish-button[aria-disabled=true]:hover {
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
|
||||
.char-limit-radial {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.option-input:focus+.delete-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-input:not(:focus)+.delete-button+.char-limit-radial {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.char-limit-radial {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { formatTimeAgo } from '@vueuse/core'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const route = useRoute()
|
||||
const { formatNumber } = useHumanReadableNumber()
|
||||
|
@ -20,35 +21,39 @@ watchEffect(() => {
|
|||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
|
||||
function firstDraftItemOf(drafts: DraftItem | Array<DraftItem>): DraftItem {
|
||||
if (Array.isArray(drafts))
|
||||
return drafts[0]
|
||||
return drafts
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col" pt-6 h-screen>
|
||||
<div flex="~ col" pb-6>
|
||||
<div inline-flex justify-end h-8>
|
||||
<VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end">
|
||||
<button btn-text flex="inline center">
|
||||
{{ $t('compose.drafts', nonEmptyDrafts.length, { named: { v: formatNumber(nonEmptyDrafts.length) } }) }} <div aria-hidden="true" i-ri:arrow-down-s-line />
|
||||
{{ $t('compose.drafts', nonEmptyDrafts.length, { named: { v: formatNumber(nonEmptyDrafts.length) } }) }} 
|
||||
<div aria-hidden="true" i-ri:arrow-down-s-line />
|
||||
</button>
|
||||
<template #popper="{ hide }">
|
||||
<div flex="~ col">
|
||||
<NuxtLink
|
||||
v-for="[key, draft] of nonEmptyDrafts" :key="key"
|
||||
border="b base" text-left py2 px4 hover:bg-active
|
||||
:replace="true"
|
||||
:to="`/compose?draft=${encodeURIComponent(key)}`"
|
||||
@click="hide()"
|
||||
v-for="[key, drafts] of nonEmptyDrafts" :key="key" border="b base" text-left py2 px4
|
||||
hover:bg-active :replace="true" :to="`/compose?draft=${encodeURIComponent(key)}`" @click="hide()"
|
||||
>
|
||||
<div>
|
||||
<div flex="~ gap-1" items-center>
|
||||
<i18n-t keypath="compose.draft_title">
|
||||
<code>{{ key }}</code>
|
||||
</i18n-t>
|
||||
<span v-if="draft.lastUpdated" text-secondary text-sm>
|
||||
· {{ formatTimeAgo(new Date(draft.lastUpdated), timeAgoOptions) }}
|
||||
<span v-if="firstDraftItemOf(drafts).lastUpdated" text-secondary text-sm>
|
||||
· {{ formatTimeAgo(new Date(firstDraftItemOf(drafts).lastUpdated), timeAgoOptions) }}
|
||||
</span>
|
||||
</div>
|
||||
<div text-secondary>
|
||||
{{ htmlToText(draft.params.status).slice(0, 50) }}
|
||||
{{ htmlToText(firstDraftItemOf(drafts).params.status).slice(0, 50) }}
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
@ -57,7 +62,7 @@ onDeactivated(() => {
|
|||
</VDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<PublishWidget :key="draftKey" expanded class="min-h-100!" :draft-key="draftKey" />
|
||||
<PublishWidgetList expanded class="min-h-100!" :draft-key="draftKey" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
49
components/publish/PublishWidgetList.vue
Normal file
49
components/publish/PublishWidgetList.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const {
|
||||
draftKey,
|
||||
initial = getDefaultDraftItem,
|
||||
expanded = false,
|
||||
placeholder,
|
||||
dialogLabelledBy,
|
||||
inReplyToId,
|
||||
inReplyToVisibility,
|
||||
} = defineProps<{
|
||||
draftKey: string
|
||||
initial?: () => DraftItem
|
||||
placeholder?: string
|
||||
inReplyToId?: string
|
||||
inReplyToVisibility?: mastodon.v1.StatusVisibility
|
||||
expanded?: boolean
|
||||
dialogLabelledBy?: string
|
||||
}>()
|
||||
|
||||
const threadItems = computed(() =>
|
||||
useThreadComposer(draftKey, initial).threadItems.value,
|
||||
)
|
||||
|
||||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
|
||||
function isFirstItem(index: number) {
|
||||
return index === 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isHydrated && currentUser">
|
||||
<PublishWidget
|
||||
v-for="(_, index) in threadItems" :key="`${draftKey}-${index}`"
|
||||
:draft-key="draftKey"
|
||||
:draft-item-index="index"
|
||||
:expanded="isFirstItem(index) ? expanded : true"
|
||||
:placeholder="placeholder"
|
||||
:dialog-labelled-by="dialogLabelledBy"
|
||||
:in-reply-to-id="isFirstItem(index) ? inReplyToId : undefined"
|
||||
:in-reply-to-visibility="inReplyToVisibility"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
|
@ -10,7 +10,8 @@ function reorderAndFilter(items: mastodon.v1.Status[]) {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<PublishWidget draft-key="home" border="b base" />
|
||||
<PublishWidgetList draft-key="home" />
|
||||
<div h="1px" w-auto bg-border mb-3 />
|
||||
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderAndFilter" context="home" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue