2022-11-21 06:55:31 +00:00
|
|
|
|
<script setup lang="ts">
|
2022-11-25 13:21:02 +00:00
|
|
|
|
import { EditorContent } from '@tiptap/vue-3'
|
2023-01-19 10:27:08 +00:00
|
|
|
|
import stringLength from 'string-length'
|
2023-01-10 13:22:39 +00:00
|
|
|
|
import type { mastodon } from 'masto'
|
2024-04-08 10:53:26 +01:00
|
|
|
|
import type { DraftItem } from '~/types'
|
2022-11-21 06:55:31 +00:00
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
draftKey,
|
2024-04-08 10:53:26 +01:00
|
|
|
|
draftItemIndex,
|
2023-01-10 13:22:39 +00:00
|
|
|
|
expanded = false,
|
2022-11-30 04:50:29 +00:00
|
|
|
|
placeholder,
|
2022-12-16 19:55:31 +00:00
|
|
|
|
dialogLabelledBy,
|
2024-04-08 10:53:26 +01:00
|
|
|
|
initial = getDefaultDraftItem,
|
2022-11-21 06:55:31 +00:00
|
|
|
|
} = defineProps<{
|
2024-04-08 10:53:26 +01:00
|
|
|
|
draftKey: string
|
|
|
|
|
draftItemIndex: number
|
|
|
|
|
initial?: () => DraftItem
|
2022-11-21 06:55:31 +00:00
|
|
|
|
placeholder?: string
|
|
|
|
|
inReplyToId?: string
|
2023-01-08 06:21:09 +00:00
|
|
|
|
inReplyToVisibility?: mastodon.v1.StatusVisibility
|
2022-11-24 15:01:45 +00:00
|
|
|
|
expanded?: boolean
|
2022-12-16 19:55:31 +00:00
|
|
|
|
dialogLabelledBy?: string
|
2022-11-21 06:55:31 +00:00
|
|
|
|
}>()
|
|
|
|
|
|
2023-01-01 15:57:49 +00:00
|
|
|
|
const emit = defineEmits<{
|
2023-01-08 06:21:09 +00:00
|
|
|
|
(evt: 'published', status: mastodon.v1.Status): void
|
2023-01-01 15:57:49 +00:00
|
|
|
|
}>()
|
2022-12-01 07:24:35 +00:00
|
|
|
|
|
2022-11-30 04:50:29 +00:00
|
|
|
|
const { t } = useI18n()
|
2023-01-09 10:04:18 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
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)
|
2022-11-28 10:23:33 +00:00
|
|
|
|
|
2023-01-10 13:22:39 +00:00
|
|
|
|
const {
|
2024-03-05 14:48:58 +00:00
|
|
|
|
isExceedingAttachmentLimit,
|
|
|
|
|
isUploading,
|
|
|
|
|
failedAttachments,
|
|
|
|
|
isOverDropZone,
|
|
|
|
|
uploadAttachments,
|
|
|
|
|
pickAttachments,
|
|
|
|
|
setDescription,
|
|
|
|
|
removeAttachment,
|
2023-01-10 22:51:25 +00:00
|
|
|
|
dropZoneRef,
|
2024-02-21 15:20:08 +00:00
|
|
|
|
} = useUploadMediaAttachment(draft)
|
2023-01-10 13:22:39 +00:00
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
const { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage, publishSpoilerText } = usePublish(
|
2023-01-10 13:22:39 +00:00
|
|
|
|
{
|
2024-04-08 10:53:26 +01:00
|
|
|
|
draftItem: draft,
|
|
|
|
|
...{ expanded: toRef(() => expanded), isUploading, initialDraft: initial, isPartOfThread: false },
|
2023-01-10 13:22:39 +00:00
|
|
|
|
},
|
2024-02-21 15:20:08 +00:00
|
|
|
|
)
|
2022-11-24 06:54:54 +00:00
|
|
|
|
|
2022-11-25 13:21:02 +00:00
|
|
|
|
const { editor } = useTiptap({
|
2022-11-25 18:10:17 +00:00
|
|
|
|
content: computed({
|
2024-02-21 15:20:08 +00:00
|
|
|
|
get: () => draft.value.params.status,
|
2023-01-05 15:42:36 +00:00
|
|
|
|
set: (newVal) => {
|
2024-02-21 15:20:08 +00:00
|
|
|
|
draft.value.params.status = newVal
|
|
|
|
|
draft.value.lastUpdated = Date.now()
|
2023-01-05 15:42:36 +00:00
|
|
|
|
},
|
2022-11-25 18:10:17 +00:00
|
|
|
|
}),
|
2024-02-21 15:20:08 +00:00
|
|
|
|
placeholder: computed(() => placeholder ?? draft.value.params.inReplyToId ? t('placeholder.replying') : t('placeholder.default_1')),
|
|
|
|
|
autofocus: shouldExpanded.value,
|
2022-11-25 14:07:31 +00:00
|
|
|
|
onSubmit: publish,
|
2022-12-12 22:35:59 +00:00
|
|
|
|
onFocus() {
|
2024-02-21 15:20:08 +00:00
|
|
|
|
if (!isExpanded && draft.value.initialText) {
|
|
|
|
|
editor.value?.chain().insertContent(`${draft.value.initialText} `).focus('end').run()
|
|
|
|
|
draft.value.initialText = ''
|
2022-12-12 22:35:59 +00:00
|
|
|
|
}
|
2024-02-21 15:20:08 +00:00
|
|
|
|
isExpanded.value = true
|
2022-12-12 22:35:59 +00:00
|
|
|
|
},
|
2022-11-25 13:21:02 +00:00
|
|
|
|
onPaste: handlePaste,
|
|
|
|
|
})
|
2023-01-30 11:09:04 +00:00
|
|
|
|
|
2023-05-21 12:37:33 +01:00
|
|
|
|
function trimPollOptions() {
|
2024-02-21 15:20:08 +00:00
|
|
|
|
const indexLastNonEmpty = draft.value.params.poll!.options.findLastIndex(option => option.trim().length > 0)
|
|
|
|
|
const trimmedOptions = draft.value.params.poll!.options.slice(0, indexLastNonEmpty + 1)
|
2023-05-21 12:37:33 +01:00
|
|
|
|
|
|
|
|
|
if (currentInstance.value?.configuration
|
2024-03-05 14:48:58 +00:00
|
|
|
|
&& trimmedOptions.length >= currentInstance.value?.configuration?.polls.maxOptions)
|
2024-02-21 15:20:08 +00:00
|
|
|
|
draft.value.params.poll!.options = trimmedOptions
|
2023-05-21 12:37:33 +01:00
|
|
|
|
else
|
2024-02-21 15:20:08 +00:00
|
|
|
|
draft.value.params.poll!.options = [...trimmedOptions, '']
|
2023-05-21 12:37:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-20 20:23:41 +01:00
|
|
|
|
function editPollOptionDraft(event: Event, index: number) {
|
2024-02-21 15:20:08 +00:00
|
|
|
|
draft.value.params.poll!.options = Object.assign(draft.value.params.poll!.options.slice(), { [index]: (event.target as HTMLInputElement).value })
|
2024-01-09 08:56:15 +00:00
|
|
|
|
|
2023-05-21 12:37:33 +01:00
|
|
|
|
trimPollOptions()
|
2023-05-20 20:23:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deletePollOption(index: number) {
|
2024-03-30 04:18:33 +00:00
|
|
|
|
const newPollOptions = draft.value.params.poll!.options.slice()
|
|
|
|
|
newPollOptions.splice(index, 1)
|
|
|
|
|
draft.value.params.poll!.options = newPollOptions
|
2023-05-21 12:37:33 +01:00
|
|
|
|
trimPollOptions()
|
2023-05-20 20:23:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 11:33:26 +01:00
|
|
|
|
const expiresInOptions = computed(() => [
|
2023-05-20 20:23:41 +01:00
|
|
|
|
{
|
|
|
|
|
seconds: 1 * 60 * 60,
|
2024-02-24 18:24:19 +00:00
|
|
|
|
label: t('time_ago_options.hour_future', 1),
|
2023-05-20 20:23:41 +01:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
seconds: 2 * 60 * 60,
|
2024-02-24 18:24:19 +00:00
|
|
|
|
label: t('time_ago_options.hour_future', 2),
|
2023-05-20 20:23:41 +01:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
seconds: 1 * 24 * 60 * 60,
|
2024-02-24 18:24:19 +00:00
|
|
|
|
label: t('time_ago_options.day_future', 1),
|
2023-05-20 20:23:41 +01:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
seconds: 2 * 24 * 60 * 60,
|
2024-02-24 18:24:19 +00:00
|
|
|
|
label: t('time_ago_options.day_future', 2),
|
2023-05-20 20:23:41 +01:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
seconds: 7 * 24 * 60 * 60,
|
2024-02-24 18:24:19 +00:00
|
|
|
|
label: t('time_ago_options.day_future', 7),
|
2023-05-20 20:23:41 +01:00
|
|
|
|
},
|
2023-08-23 11:33:26 +01:00
|
|
|
|
])
|
2023-05-20 20:23:41 +01:00
|
|
|
|
|
|
|
|
|
const expiresInDefaultOptionIndex = 2
|
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
const characterCount = computed(() => {
|
2023-02-06 23:03:16 +00:00
|
|
|
|
const text = htmlToText(editor.value?.getHTML() || '')
|
|
|
|
|
|
|
|
|
|
let length = stringLength(text)
|
|
|
|
|
|
|
|
|
|
// taken from https://github.com/mastodon/mastodon/blob/07f8b4d1b19f734d04e69daeb4c3421ef9767aac/app/lib/text_formatter.rb
|
|
|
|
|
const linkRegex = /(https?:\/\/(www\.)?|xmpp:)\S+/g
|
|
|
|
|
|
2023-03-02 20:07:28 +00:00
|
|
|
|
// taken from https://github.com/mastodon/mastodon/blob/af578e/app/javascript/mastodon/features/compose/util/counter.js
|
|
|
|
|
const countableMentionRegex = /(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig
|
|
|
|
|
|
2023-02-06 23:03:16 +00:00
|
|
|
|
// maximum of 23 chars per link
|
|
|
|
|
// https://github.com/elk-zone/elk/issues/1651
|
|
|
|
|
const maxLength = 23
|
|
|
|
|
|
|
|
|
|
for (const [fullMatch] of text.matchAll(linkRegex))
|
|
|
|
|
length -= fullMatch.length - Math.min(maxLength, fullMatch.length)
|
2023-01-16 10:24:29 +00:00
|
|
|
|
|
2023-03-19 12:12:20 +00:00
|
|
|
|
for (const [fullMatch, before, _handle, username] of text.matchAll(countableMentionRegex))
|
2023-03-02 20:07:28 +00:00
|
|
|
|
length -= fullMatch.length - (before + username).length - 1 // - 1 for the @
|
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
if (draft.value.mentions) {
|
2024-03-12 07:58:20 +00:00
|
|
|
|
// + 1 is needed as mentions always need a space separator at the end
|
2024-02-21 15:20:08 +00:00
|
|
|
|
length += draft.value.mentions.map((mention) => {
|
2023-01-16 10:24:29 +00:00
|
|
|
|
const [handle] = mention.split('@')
|
|
|
|
|
return `@${handle}`
|
|
|
|
|
}).join(' ').length + 1
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
length += stringLength(publishSpoilerText.value)
|
2023-02-05 17:36:33 +00:00
|
|
|
|
|
2023-01-16 10:24:29 +00:00
|
|
|
|
return length
|
|
|
|
|
})
|
2022-11-23 17:17:54 +00:00
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
const isExceedingCharacterLimit = computed(() => {
|
|
|
|
|
return characterCount.value > characterLimit.value
|
2023-03-06 13:12:31 +00:00
|
|
|
|
})
|
|
|
|
|
|
2024-02-21 15:20:08 +00:00
|
|
|
|
const postLanguageDisplay = computed(() => languagesNameList.find(i => i.code === (draft.value.params.language || preferredLanguage))?.nativeName)
|
2023-01-30 11:09:04 +00:00
|
|
|
|
|
2024-04-01 06:26:51 +01:00
|
|
|
|
const isDM = computed(() => draft.value.params.visibility === 'direct')
|
|
|
|
|
|
2022-11-23 17:17:54 +00:00
|
|
|
|
async function handlePaste(evt: ClipboardEvent) {
|
|
|
|
|
const files = evt.clipboardData?.files
|
2022-11-24 08:20:21 +00:00
|
|
|
|
if (!files || files.length === 0)
|
2022-11-23 17:17:54 +00:00
|
|
|
|
return
|
|
|
|
|
|
2022-11-24 04:05:13 +00:00
|
|
|
|
evt.preventDefault()
|
2022-11-23 17:17:54 +00:00
|
|
|
|
await uploadAttachments(Array.from(files))
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-27 19:13:50 +00:00
|
|
|
|
function insertEmoji(name: string) {
|
|
|
|
|
editor.value?.chain().focus().insertEmoji(name).run()
|
2022-12-23 19:15:19 +00:00
|
|
|
|
}
|
2022-12-27 19:13:50 +00:00
|
|
|
|
function insertCustomEmoji(image: any) {
|
|
|
|
|
editor.value?.chain().focus().insertCustomEmoji(image).run()
|
2022-12-27 18:38:57 +00:00
|
|
|
|
}
|
2022-12-23 19:15:19 +00:00
|
|
|
|
|
2022-11-25 12:10:45 +00:00
|
|
|
|
async function toggleSensitive() {
|
2024-02-21 15:20:08 +00:00
|
|
|
|
draft.value.params.sensitive = !draft.value.params.sensitive
|
2022-11-25 12:10:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 06:55:31 +00:00
|
|
|
|
async function publish() {
|
2024-04-08 10:53:26 +01:00
|
|
|
|
const publishResult = await (threadIsActive.value ? publishThread() : publishDraft())
|
|
|
|
|
if (publishResult) {
|
|
|
|
|
if (Array.isArray(publishResult))
|
|
|
|
|
failedMessages.value = publishResult
|
|
|
|
|
else
|
|
|
|
|
emit('published', publishResult)
|
|
|
|
|
}
|
2022-11-21 06:55:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 20:58:52 +00:00
|
|
|
|
useWebShareTarget(async ({ data: { data, action } }: any) => {
|
|
|
|
|
if (action !== 'compose-with-shared-data')
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
editor.value?.commands.focus('end')
|
|
|
|
|
|
2023-02-03 14:20:32 +00:00
|
|
|
|
for (const text of data.textParts) {
|
|
|
|
|
for (const line of text.split('\n')) {
|
|
|
|
|
editor.value?.commands.insertContent({
|
|
|
|
|
type: 'paragraph',
|
|
|
|
|
content: [{ type: 'text', text: line }],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-14 20:58:52 +00:00
|
|
|
|
|
2023-02-03 14:20:32 +00:00
|
|
|
|
if (data.files.length !== 0)
|
2023-01-14 20:58:52 +00:00
|
|
|
|
await uploadAttachments(data.files)
|
|
|
|
|
})
|
|
|
|
|
|
2022-12-14 16:45:46 +00:00
|
|
|
|
defineExpose({
|
|
|
|
|
focusEditor: () => {
|
|
|
|
|
editor.value?.commands?.focus?.()
|
|
|
|
|
},
|
|
|
|
|
})
|
2023-04-27 21:41:10 +01:00
|
|
|
|
|
2023-07-10 10:29:42 +01:00
|
|
|
|
function stopQuestionMarkPropagation(e: KeyboardEvent) {
|
|
|
|
|
if (e.key === '?')
|
|
|
|
|
e.stopImmediatePropagation()
|
|
|
|
|
}
|
2022-11-21 06:55:31 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2023-03-07 19:32:21 +00:00
|
|
|
|
<div v-if="isHydrated && currentUser" flex="~ col gap-4" py3 px2 sm:px4 aria-roledescription="publish-widget">
|
2022-11-24 11:35:26 +00:00
|
|
|
|
<template v-if="draft.editingStatus">
|
2023-07-29 19:38:13 +01:00
|
|
|
|
<div id="state-editing" text-secondary self-center>
|
|
|
|
|
{{ $t('state.editing') }}
|
2022-11-24 07:53:27 +00:00
|
|
|
|
</div>
|
2022-11-24 11:35:26 +00:00
|
|
|
|
</template>
|
2022-12-27 19:13:50 +00:00
|
|
|
|
<div flex gap-3 flex-1>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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 />
|
2022-11-25 12:10:45 +00:00
|
|
|
|
</div>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</div>
|
2022-11-25 12:10:45 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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
|
|
|
|
|
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>
|
2023-01-17 12:56:51 +00:00
|
|
|
|
<button
|
2024-04-08 10:53:26 +01:00
|
|
|
|
v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red
|
|
|
|
|
@click="draft.mentions?.splice(i, 1)"
|
2023-01-17 12:56:51 +00:00
|
|
|
|
>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
{{ accountToShortHandle(m) }}
|
2023-01-17 12:56:51 +00:00
|
|
|
|
</button>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</ContentMentionGroup>
|
2022-11-24 07:53:27 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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"
|
2022-12-26 05:25:35 +00:00
|
|
|
|
>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</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>
|
2022-12-31 09:02:55 +00:00
|
|
|
|
</div>
|
2022-11-24 11:35:26 +00:00
|
|
|
|
</div>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
|
|
|
|
|
<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)"
|
2023-05-20 20:23:41 +01:00
|
|
|
|
>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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>
|
|
|
|
|
<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>
|
|
|
|
|
</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 />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</button>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</PublishEmojiPicker>
|
|
|
|
|
|
|
|
|
|
<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 />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</button>
|
|
|
|
|
</CommonTooltip>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
|
|
|
|
|
<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 />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</button>
|
|
|
|
|
</CommonTooltip>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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 />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</button>
|
|
|
|
|
</CommonTooltip>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<PublishEditorTools v-if="editor" :editor="editor" />
|
2022-11-24 09:15:58 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<div flex-auto />
|
2022-11-25 13:21:02 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
2022-12-30 06:10:29 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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>
|
2023-01-01 21:52:00 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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 />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</button>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</CommonTooltip>
|
2023-01-01 21:52:00 +00:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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>
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</template>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
</PublishVisibilityPicker>
|
|
|
|
|
|
|
|
|
|
<PublishThreadTools :draft-item-index="draftItemIndex" :draft-key="draftKey" />
|
2023-05-20 20:23:41 +01:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<CommonTooltip
|
|
|
|
|
v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top"
|
|
|
|
|
:content="$t('tooltip.publish_failed')"
|
2023-05-20 20:23:41 +01:00
|
|
|
|
>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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)"
|
2023-05-20 20:23:41 +01:00
|
|
|
|
>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
<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>
|
2023-05-20 20:23:41 +01:00
|
|
|
|
</div>
|
2022-11-24 07:53:27 +00:00
|
|
|
|
</div>
|
2022-11-21 06:55:31 +00:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2023-01-08 20:55:52 +00:00
|
|
|
|
|
|
|
|
|
<style scoped>
|
2024-04-08 10:53:26 +01:00
|
|
|
|
.publish-button[aria-disabled=true] {
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
background-color: var(--c-bg-btn-disabled);
|
|
|
|
|
color: var(--c-text-btn-disabled);
|
|
|
|
|
}
|
2023-05-21 12:37:33 +01:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
.publish-button[aria-disabled=true]:hover {
|
|
|
|
|
background-color: var(--c-bg-btn-disabled);
|
|
|
|
|
color: var(--c-text-btn-disabled);
|
|
|
|
|
}
|
2023-05-21 12:37:33 +01:00
|
|
|
|
|
2024-04-08 10:53:26 +01:00
|
|
|
|
.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%;
|
|
|
|
|
}
|
2023-01-08 20:55:52 +00:00
|
|
|
|
</style>
|