elk/components/publish/PublishWidget.vue

345 lines
11 KiB
Vue
Raw Normal View History

2022-11-21 07:55:31 +01:00
<script setup lang="ts">
2022-12-15 00:30:54 +01:00
import type { Attachment, CreateStatusParams, StatusVisibility } from 'masto'
import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core'
import { EditorContent } from '@tiptap/vue-3'
2022-12-13 15:03:30 +01:00
import type { Draft } from '~/types'
2022-11-21 07:55:31 +01:00
const {
draftKey,
initial = getDefaultDraft() as never /* Bug of vue-core */,
expanded: _expanded = false,
2022-11-30 05:50:29 +01:00
placeholder,
dialogLabelledBy,
2022-11-21 07:55:31 +01:00
} = defineProps<{
draftKey: string
initial?: () => Draft
2022-11-21 07:55:31 +01:00
placeholder?: string
inReplyToId?: string
inReplyToVisibility?: StatusVisibility
expanded?: boolean
dialogLabelledBy?: string
2022-11-21 07:55:31 +01:00
}>()
2022-12-01 08:24:35 +01:00
const emit = defineEmits(['published'])
2022-11-30 05:50:29 +01:00
const { t } = useI18n()
// eslint-disable-next-line prefer-const
let { draft, isEmpty } = $(useDraft(draftKey, initial))
2022-11-21 07:55:31 +01:00
let isSending = $ref(false)
2022-11-29 12:57:05 +01:00
let isExpanded = $ref(false)
const shouldExpanded = $computed(() => _expanded || isExpanded || !isEmpty)
2022-11-24 07:54:54 +01:00
const { editor } = useTiptap({
2022-11-25 19:10:17 +01:00
content: computed({
get: () => draft.params.status,
set: newVal => draft.params.status = newVal,
}),
placeholder: computed(() => placeholder ?? draft.params.inReplyToId ? t('placeholder.replying') : t('placeholder.default_1')),
2022-11-29 12:57:05 +01:00
autofocus: shouldExpanded,
2022-11-25 15:07:31 +01:00
onSubmit: publish,
onFocus() {
if (!isExpanded && draft.initialText) {
editor.value?.chain().insertContent(`${draft.initialText} `).focus('end').run()
draft.initialText = ''
}
isExpanded = true
},
onPaste: handlePaste,
})
2022-11-24 10:15:58 +01:00
const currentVisibility = $computed(() => {
2022-11-25 17:17:15 +01:00
return STATUS_VISIBILITIES.find(v => v.value === draft.params.visibility) || STATUS_VISIBILITIES[0]
2022-11-24 10:15:58 +01:00
})
2022-11-23 18:17:54 +01:00
let isUploading = $ref<boolean>(false)
let failed = $ref<File[]>([])
2022-11-23 18:17:54 +01:00
async function handlePaste(evt: ClipboardEvent) {
const files = evt.clipboardData?.files
2022-11-24 09:20:21 +01:00
if (!files || files.length === 0)
2022-11-23 18:17:54 +01:00
return
2022-11-24 05:05:13 +01:00
evt.preventDefault()
2022-11-23 18:17:54 +01:00
await uploadAttachments(Array.from(files))
}
2022-12-23 20:15:19 +01:00
function insertText(text: string) {
editor.value?.chain().insertContent(text).focus().run()
}
2022-11-23 18:17:54 +01:00
async function pickAttachments() {
const files = await fileOpen([
{
description: 'Attachments',
multiple: true,
mimeTypes: ['image/*'],
extensions: ['.png', '.gif', '.jpeg', '.jpg', '.webp', '.avif', '.heic', '.heif'],
},
{
description: 'Attachments',
mimeTypes: ['video/*'],
extensions: ['.webm', '.mp4', '.m4v', '.mov', '.ogv', '.3gp'],
},
{
2022-11-24 05:05:13 +01:00
description: 'Attachments',
mimeTypes: ['audio/*'],
extensions: ['.mp3', '.ogg', '.oga', '.wav', '.flac', '.opus', '.aac', '.m4a', '.3gp', '.wma'],
},
])
2022-11-23 18:17:54 +01:00
await uploadAttachments(files)
}
async function toggleSensitive() {
draft.params.sensitive = !draft.params.sensitive
}
const masto = useMasto()
2022-11-23 18:17:54 +01:00
async function uploadAttachments(files: File[]) {
isUploading = true
failed = []
2022-11-23 18:17:54 +01:00
for (const file of files) {
try {
const attachment = await masto.mediaAttachments.create({
file,
})
draft.attachments.push(attachment)
}
catch (e) {
// TODO: add some human-readable error message, problem is that masto api will not return response code
console.error(e)
failed = [...failed, file]
}
2022-11-23 18:17:54 +01:00
}
isUploading = false
}
2022-12-15 00:30:54 +01:00
async function setDescription(att: Attachment, description: string) {
att.description = description
await masto.mediaAttachments.update(att.id, { description: att.description })
2022-12-15 00:30:54 +01:00
}
2022-11-24 05:05:13 +01:00
function removeAttachment(index: number) {
2022-11-24 07:54:54 +01:00
draft.attachments.splice(index, 1)
2022-11-23 18:17:54 +01:00
}
2022-11-21 07:55:31 +01:00
2022-11-24 10:15:58 +01:00
function chooseVisibility(visibility: StatusVisibility) {
draft.params.visibility = visibility
}
2022-11-21 07:55:31 +01:00
async function publish() {
2022-11-25 17:17:15 +01:00
const payload = {
...draft.params,
status: htmlToText(draft.params.status || ''),
mediaIds: draft.attachments.map(a => a.id),
} as CreateStatusParams
if (process.dev) {
2022-11-25 17:17:15 +01:00
// eslint-disable-next-line no-console
console.info({
raw: draft.params.status,
...payload,
})
// eslint-disable-next-line no-alert
2022-11-25 17:17:15 +01:00
const result = confirm('[DEV] Payload logged to console, do you want to publish it?')
if (!result)
return
}
2022-11-25 17:17:15 +01:00
2022-11-21 07:55:31 +01:00
try {
isSending = true
2022-11-25 17:17:15 +01:00
2022-11-24 12:35:26 +01:00
if (!draft.editingStatus)
await masto.statuses.create(payload)
else
await masto.statuses.update(draft.editingStatus.id, payload)
2022-11-24 12:35:26 +01:00
draft = initial()
2022-11-24 12:35:26 +01:00
isPublishDialogOpen.value = false
2022-12-01 08:24:35 +01:00
emit('published')
2022-11-21 07:55:31 +01:00
}
finally {
isSending = false
}
}
const dropZoneRef = ref<HTMLDivElement>()
async function onDrop(files: File[] | null) {
if (files)
await uploadAttachments(files)
}
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
defineExpose({
focusEditor: () => {
editor.value?.commands?.focus?.()
},
})
2022-11-21 07:55:31 +01:00
</script>
<template>
<div v-if="isMastoInitialised && currentUser" flex="~ col gap-4" py3 px2 sm:px4>
2022-11-24 12:35:26 +01:00
<template v-if="draft.editingStatus">
<div flex="~ col gap-1">
<div id="state-editing" text-secondary self-center>
2022-11-29 07:57:32 +01:00
{{ $t('state.editing') }}
2022-11-24 12:35:26 +01:00
</div>
2022-12-02 08:02:44 +01:00
<StatusCard :status="draft.editingStatus" :actions="false" :hover="false" px-0 />
2022-11-24 08:53:27 +01:00
</div>
2022-11-24 12:35:26 +01:00
<div border="b dashed gray/40" />
</template>
2022-11-24 15:32:20 +01:00
<div flex gap-4 flex-1>
<NuxtLink :to="getAccountRoute(currentUser.account)">
<AccountAvatar :account="currentUser.account" account-avatar-normal />
2022-11-24 16:19:18 +01:00
</NuxtLink>
<!-- This `w-0` style is used to avoid overflow problems in flex layoutsso don't remove it unless you know what you're doing -->
2022-11-24 12:35:26 +01:00
<div
ref="dropZoneRef"
flex w-0 flex-col gap-3 flex-1
border="2 dashed transparent"
2022-11-25 10:31:32 +01:00
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
2022-11-24 12:35:26 +01:00
>
<div v-if="draft.params.sensitive">
<input
v-model="draft.params.spoilerText"
type="text"
:placeholder="$t('placeholder.content_warning')"
p2 border-rounded w-full bg-transparent
outline-none border="~ base"
>
</div>
<div relative flex-1 flex flex-col>
2022-11-25 14:29:42 +01:00
<EditorContent
:editor="editor"
2022-12-14 14:22:35 +01:00
flex max-w-full
:class="shouldExpanded ? 'min-h-30 md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain' : ''"
2022-11-25 14:29:42 +01:00
/>
2022-11-29 12:57:05 +01:00
<div v-if="shouldExpanded" absolute right-0 bottom-0 pointer-events-none text-sm text-secondary-light>
{{ characterLimit - editor?.storage.characterCount.characters() }}
2022-11-25 14:29:42 +01:00
</div>
</div>
2022-11-24 08:53:27 +01:00
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
<div i-ri:loader-2-fill animate-spin />
2022-11-29 07:57:32 +01:00
{{ $t('state.uploading') }}
</div>
<div
v-else-if="failed.length > 0"
role="alert"
aria-describedby="upload-failed"
flex="~ col"
gap-1 text-sm pt-1 pl-2 pr-1 pb-2 text-red-600 dark:text-red-400
border="~ base rounded red-600 dark:red-400"
>
<head 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="failed = []"
>
<span aria-hidden="true" w-1.75em h-1.75em i-ri:close-line />
</button>
</CommonTooltip>
</head>
<ol pl-2 sm:pl-1>
<li v-for="file in failed" :key="file.name">
{{ file.name }}
</li>
</ol>
</div>
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
2022-11-24 12:35:26 +01:00
<PublishAttachment
v-for="(att, idx) in draft.attachments" :key="att.id"
:attachment="att"
:dialog-labelled-by="dialogLabelledBy ?? (draft.editingStatus ? 'state-editing' : null)"
2022-11-24 12:35:26 +01:00
@remove="removeAttachment(idx)"
2022-12-15 00:30:54 +01:00
@set-description="setDescription(att, $event)"
2022-11-24 12:35:26 +01:00
/>
</div>
2022-12-02 08:02:44 +01:00
</div>
</div>
<div flex gap-4>
<div w-12 h-full sm:block hidden />
<div
v-if="shouldExpanded" flex="~ gap-2 1" m="l--1" pt-2 justify="between" max-full
border="t base"
>
2022-12-23 20:15:19 +01:00
<PublishEmojiPicker @select="insertText" />
2022-12-02 08:02:44 +01:00
<CommonTooltip placement="bottom" :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="editor">
<CommonTooltip placement="bottom" :content="$t('tooltip.toggle_code_block')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_code_block')"
:class="editor.isActive('codeBlock') ? 'op100' : 'op50'"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
2022-11-24 16:48:52 +01:00
</button>
</CommonTooltip>
2022-12-02 08:02:44 +01:00
</template>
2022-11-24 10:15:58 +01:00
2022-12-02 08:02:44 +01:00
<div flex-auto />
2022-12-02 08:02:44 +01:00
<CommonTooltip placement="bottom" :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>
2022-11-24 14:26:33 +01:00
2022-12-02 08:02:44 +01:00
<CommonTooltip placement="bottom" :content="$t('tooltip.change_content_visibility')">
<CommonDropdown>
<button :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon w-12>
<div :class="currentVisibility.icon" />
<div i-ri:arrow-down-s-line text-sm text-secondary mr--1 />
</button>
2022-11-24 12:35:26 +01:00
2022-12-02 08:02:44 +01:00
<template #popper>
<CommonDropdownItem
v-for="visibility in STATUS_VISIBILITIES"
:key="visibility.value"
:icon="visibility.icon"
:checked="visibility.value === draft.params.visibility"
@click="chooseVisibility(visibility.value)"
>
{{ $t(`visibility.${visibility.value}`) }}
<template #description>
{{ $t(`visibility.${visibility.value}_desc`) }}
</template>
</CommonDropdownItem>
</template>
</CommonDropdown>
</CommonTooltip>
<button
btn-solid rounded-full text-sm
:disabled="isEmpty || isUploading || (draft.attachments.length === 0 && !draft.params.status)"
@click="publish"
>
{{ !draft.editingStatus ? $t('action.publish') : $t('action.save_changes') }}
</button>
2022-11-24 08:53:27 +01:00
</div>
2022-11-21 07:55:31 +01:00
</div>
</div>
</template>