refactor(publish): extract to composables

zio/stable
三咲智子 Kevin Deng 2023-01-10 21:22:39 +08:00
parent df37e7c4de
commit 0ef99f2c8e
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
5 changed files with 190 additions and 122 deletions

View File

@ -1,16 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto'
import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core'
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
import type { mastodon } from 'masto'
import type { Ref } from 'vue'
import type { Draft } from '~/types' import type { Draft } from '~/types'
type FileUploadError = [filename: string, message: string]
const { const {
draftKey, draftKey,
initial = getDefaultDraft() as never /* Bug of vue-core */, initial = getDefaultDraft() as never /* Bug of vue-core */,
expanded: _expanded = false, expanded = false,
placeholder, placeholder,
dialogLabelledBy, dialogLabelledBy,
} = defineProps<{ } = defineProps<{
@ -29,11 +26,20 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
let { draft, isEmpty } = $(useDraft(draftKey, initial)) const draftState = useDraft(draftKey, initial)
const { draft } = $(draftState)
let isSending = $ref(false) const {
let isExpanded = $ref(false) isExceedingAttachmentLimit, isUploading, failedAttachments, isOverDropZone,
const shouldExpanded = $computed(() => _expanded || isExpanded || !isEmpty) uploadAttachments, pickAttachments, setDescription, removeAttachment,
} = $(useUploadMediaAttachment($$(draft)))
let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft } = $(usePublish(
{
draftState,
...$$({ expanded, isUploading, initialDraft: initial }),
},
))
const { editor } = useTiptap({ const { editor } = useTiptap({
content: computed({ content: computed({
@ -55,11 +61,7 @@ const { editor } = useTiptap({
}, },
onPaste: handlePaste, onPaste: handlePaste,
}) })
const characterCount = $computed(() => htmlToText(editor.value?.getHTML() || '').length) const characterCount = $computed(() => htmlToText(editor.value?.getHTML() || '').length)
let isUploading = $ref<boolean>(false)
let isExceedingAttachmentLimit = $ref<boolean>(false)
let failed = $ref<FileUploadError[]>([])
async function handlePaste(evt: ClipboardEvent) { async function handlePaste(evt: ClipboardEvent) {
const files = evt.clipboardData?.files const files = evt.clipboardData?.files
@ -77,120 +79,21 @@ function insertCustomEmoji(image: any) {
editor.value?.chain().focus().insertCustomEmoji(image).run() editor.value?.chain().focus().insertCustomEmoji(image).run()
} }
async function pickAttachments() {
const mimeTypes = currentInstance.value!.configuration.mediaAttachments.supportedMimeTypes
const files = await fileOpen({
description: 'Attachments',
multiple: true,
mimeTypes,
})
await uploadAttachments(files)
}
async function toggleSensitive() { async function toggleSensitive() {
draft.params.sensitive = !draft.params.sensitive draft.params.sensitive = !draft.params.sensitive
} }
const masto = useMasto()
async function uploadAttachments(files: File[]) {
isUploading = true
failed = []
// TODO: display some kind of message if too many media are selected
// DONE
const limit = currentInstance.value!.configuration.statuses.maxMediaAttachments || 4
for (const file of files.slice(0, limit)) {
if (draft.attachments.length < limit) {
isExceedingAttachmentLimit = false
try {
const attachment = await masto.v1.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.name, (e as Error).message]]
}
}
else {
isExceedingAttachmentLimit = true
failed = [...failed, [file.name, t('state.attachments_limit_error')]]
}
}
isUploading = false
}
async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
att.description = description
await masto.v1.mediaAttachments.update(att.id, { description: att.description })
}
function removeAttachment(index: number) {
draft.attachments.splice(index, 1)
}
async function publish() { async function publish() {
const payload = { const status = await publishDraft()
...draft.params, if (status)
status: htmlToText(draft.params.status || ''),
mediaIds: draft.attachments.map(a => a.id),
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
} as mastodon.v1.CreateStatusParams
if (process.dev) {
// eslint-disable-next-line no-console
console.info({
raw: draft.params.status,
...payload,
})
// eslint-disable-next-line no-alert
const result = confirm('[DEV] Payload logged to console, do you want to publish it?')
if (!result)
return
}
try {
isSending = true
let status: mastodon.v1.Status
if (!draft.editingStatus)
status = await masto.v1.statuses.create(payload)
else
status = await masto.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId)
navigateToStatus({ status })
draft = initial()
emit('published', status) emit('published', status)
}
finally {
isSending = false
}
} }
const dropZoneRef = ref<HTMLDivElement>()
async function onDrop(files: File[] | null) {
if (files)
await uploadAttachments(files)
}
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
defineExpose({ defineExpose({
focusEditor: () => { focusEditor: () => {
editor.value?.commands?.focus?.() editor.value?.commands?.focus?.()
}, },
}) })
const isPublishDisabled = computed(() => {
if (isEmpty || isUploading || (draft.attachments.length === 0 && !draft.params.status))
return true
return false
})
</script> </script>
<template> <template>
@ -239,7 +142,7 @@ const isPublishDisabled = computed(() => {
{{ $t('state.uploading') }} {{ $t('state.uploading') }}
</div> </div>
<div <div
v-else-if="failed.length > 0" v-else-if="failedAttachments.length > 0"
role="alert" role="alert"
:aria-describedby="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'" :aria-describedby="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
flex="~ col" flex="~ col"
@ -258,7 +161,7 @@ const isPublishDisabled = computed(() => {
flex rounded-4 p1 flex rounded-4 p1
hover:bg-active cursor-pointer transition-100 hover:bg-active cursor-pointer transition-100
:aria-label="$t('action.clear_upload_failed')" :aria-label="$t('action.clear_upload_failed')"
@click="failed = []" @click="failedAttachments = []"
> >
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
@ -268,7 +171,7 @@ const isPublishDisabled = computed(() => {
{{ $t('state.attachments_exceed_server_limit') }} {{ $t('state.attachments_exceed_server_limit') }}
</div> </div>
<ol ps-2 sm:ps-1> <ol ps-2 sm:ps-1>
<li v-for="error in failed" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2> <li v-for="error in failedAttachments" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong>{{ error[1] }}:</strong> <strong>{{ error[1] }}:</strong>
<span>{{ error[0] }}</span> <span>{{ error[0] }}</span>
</li> </li>

View File

@ -0,0 +1,153 @@
import { fileOpen } from 'browser-fs-access'
import type { Ref } from 'vue'
import type { mastodon } from 'masto'
import type { UseDraft } from './statusDrafts'
import type { Draft } from '~~/types'
export const usePublish = (options: {
draftState: UseDraft
expanded: Ref<boolean>
isUploading: Ref<boolean>
initialDraft: Ref<() => Draft>
}) => {
const { expanded, isUploading, initialDraft } = $(options)
let { draft, isEmpty } = $(options.draftState)
const masto = useMasto()
let isSending = $ref(false)
const isExpanded = $ref(false)
const shouldExpanded = $computed(() => expanded || isExpanded || !isEmpty)
const isPublishDisabled = $computed(() => {
return isEmpty || isUploading || isSending || (draft.attachments.length === 0 && !draft.params.status)
})
async function publishDraft() {
const payload = {
...draft.params,
status: htmlToText(draft.params.status || ''),
mediaIds: draft.attachments.map(a => a.id),
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
} as mastodon.v1.CreateStatusParams
if (process.dev) {
// eslint-disable-next-line no-console
console.info({
raw: draft.params.status,
...payload,
})
// eslint-disable-next-line no-alert
const result = confirm('[DEV] Payload logged to console, do you want to publish it?')
if (!result)
return
}
try {
isSending = true
let status: mastodon.v1.Status
if (!draft.editingStatus)
status = await masto.v1.statuses.create(payload)
else
status = await masto.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId)
navigateToStatus({ status })
draft = initialDraft()
return status
}
finally {
isSending = false
}
}
return $$({
isSending,
isExpanded,
shouldExpanded,
isPublishDisabled,
publishDraft,
})
}
export type MediaAttachmentUploadError = [filename: string, message: string]
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
const draft = $(draftRef)
const masto = useMasto()
const { t } = useI18n()
let isUploading = $ref<boolean>(false)
let isExceedingAttachmentLimit = $ref<boolean>(false)
let failedAttachments = $ref<MediaAttachmentUploadError[]>([])
const dropZoneRef = ref<HTMLDivElement>()
async function uploadAttachments(files: File[]) {
isUploading = true
failedAttachments = []
// TODO: display some kind of message if too many media are selected
// DONE
const limit = currentInstance.value!.configuration.statuses.maxMediaAttachments || 4
for (const file of files.slice(0, limit)) {
if (draft.attachments.length < limit) {
isExceedingAttachmentLimit = false
try {
const attachment = await masto.v1.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)
failedAttachments = [...failedAttachments, [file.name, (e as Error).message]]
}
}
else {
isExceedingAttachmentLimit = true
failedAttachments = [...failedAttachments, [file.name, t('state.attachments_limit_error')]]
}
}
isUploading = false
}
async function pickAttachments() {
const mimeTypes = currentInstance.value!.configuration.mediaAttachments.supportedMimeTypes
const files = await fileOpen({
description: 'Attachments',
multiple: true,
mimeTypes,
})
await uploadAttachments(files)
}
async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
att.description = description
await masto.v1.mediaAttachments.update(att.id, { description: att.description })
}
function removeAttachment(index: number) {
draft.attachments.splice(index, 1)
}
async function onDrop(files: File[] | null) {
if (files)
await uploadAttachments(files)
}
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
return $$({
isUploading,
isExceedingAttachmentLimit,
failedAttachments,
isOverDropZone,
uploadAttachments,
pickAttachments,
setDescription,
removeAttachment,
})
}

View File

@ -63,3 +63,12 @@ export function getStatusInReplyToRoute(status: mastodon.v1.Status) {
}, },
}) })
} }
export const navigateToStatus = ({ status, focusReply = false }: {
status: mastodon.v1.Status
focusReply?: boolean
}) =>
navigateTo({
path: getStatusRoute(status).href,
state: { focusReply },
})

View File

@ -1,4 +1,5 @@
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { ComputedRef, Ref } from 'vue'
import { STORAGE_KEY_DRAFTS } from '~/constants' import { STORAGE_KEY_DRAFTS } from '~/constants'
import type { Draft, DraftMap } from '~/types' import type { Draft, DraftMap } from '~/types'
import type { Mutable } from '~/types/utils' import type { Mutable } from '~/types/utils'
@ -89,10 +90,15 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
&& (params.spoilerText || '').length === 0 && (params.spoilerText || '').length === 0
} }
export interface UseDraft {
draft: Ref<Draft>
isEmpty: ComputedRef<boolean>
}
export function useDraft( export function useDraft(
draftKey?: string, draftKey?: string,
initial: () => Draft = () => getDefaultDraft({}), initial: () => Draft = () => getDefaultDraft({}),
) { ): UseDraft {
const draft = draftKey const draft = draftKey
? computed({ ? computed({
get() { get() {

View File

@ -1,3 +0,0 @@
import type { mastodon } from 'masto'
export const navigateToStatus = ({ status, focusReply = false }: { status: mastodon.v1.Status; focusReply?: boolean }) => navigateTo({ path: getStatusRoute(status).href, state: { focusReply } })