mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
paste images attachment
This commit is contained in:
@@ -129,15 +129,23 @@
|
|||||||
pollOptions = pollOptions.filter((_, optionIndex) => optionIndex !== index)
|
pollOptions = pollOptions.filter((_, optionIndex) => optionIndex !== index)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFiles(event: Event): Promise<void> {
|
async function uploadFiles(files: File[]): Promise<void> {
|
||||||
const input = event.currentTarget as HTMLInputElement
|
if (files.length === 0 || uploading) return
|
||||||
const files = Array.from(input.files ?? [])
|
if (showPoll) {
|
||||||
if (files.length === 0) return
|
error = 'Remove the poll before adding media.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableSlots = maxAttachments - attachments.length
|
||||||
|
if (availableSlots <= 0) {
|
||||||
|
error = `This server allows up to ${maxAttachments} attachments.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
uploading = true
|
uploading = true
|
||||||
error = null
|
error = null
|
||||||
try {
|
try {
|
||||||
for (const file of files.slice(0, maxAttachments - attachments.length)) {
|
for (const file of files.slice(0, availableSlots)) {
|
||||||
const media = await endpoints.uploadMedia(session.api, file)
|
const media = await endpoints.uploadMedia(session.api, file)
|
||||||
attachments = [...attachments, media]
|
attachments = [...attachments, media]
|
||||||
}
|
}
|
||||||
@@ -145,10 +153,30 @@
|
|||||||
error = cause instanceof Error ? cause.message : 'Upload failed.'
|
error = cause instanceof Error ? cause.message : 'Upload failed.'
|
||||||
} finally {
|
} finally {
|
||||||
uploading = false
|
uploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFiles(event: Event): Promise<void> {
|
||||||
|
const input = event.currentTarget as HTMLInputElement
|
||||||
|
try {
|
||||||
|
await uploadFiles(Array.from(input.files ?? []))
|
||||||
|
} finally {
|
||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clipboardImages(event: ClipboardEvent): File[] {
|
||||||
|
const itemImages = Array.from(event.clipboardData?.items ?? [])
|
||||||
|
.filter((item) => item.kind === 'file' && item.type.startsWith('image/'))
|
||||||
|
.map((item) => item.getAsFile())
|
||||||
|
.filter((file): file is File => file !== null)
|
||||||
|
|
||||||
|
if (itemImages.length > 0) return itemImages
|
||||||
|
return Array.from(event.clipboardData?.files ?? []).filter((file) =>
|
||||||
|
file.type.startsWith('image/'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function removeAttachment(id: string): void {
|
function removeAttachment(id: string): void {
|
||||||
attachments = attachments.filter((media) => media.id !== id)
|
attachments = attachments.filter((media) => media.id !== id)
|
||||||
}
|
}
|
||||||
@@ -189,22 +217,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function composerShortcuts(form: HTMLFormElement): { destroy(): void } {
|
function composerInteractions(form: HTMLFormElement): { destroy(): void } {
|
||||||
function keydown(event: KeyboardEvent): void {
|
function keydown(event: KeyboardEvent): void {
|
||||||
if (event.key !== 'Enter' || (!event.metaKey && !event.ctrlKey)) return
|
if (event.key !== 'Enter' || (!event.metaKey && !event.ctrlKey)) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (canPost) form.requestSubmit()
|
if (canPost) form.requestSubmit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paste(event: ClipboardEvent): void {
|
||||||
|
const target = event.target
|
||||||
|
if (!(target instanceof HTMLTextAreaElement) || !target.classList.contains('composer-body')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = clipboardImages(event)
|
||||||
|
if (images.length === 0) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
void uploadFiles(images)
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener('keydown', keydown)
|
form.addEventListener('keydown', keydown)
|
||||||
|
form.addEventListener('paste', paste)
|
||||||
return {
|
return {
|
||||||
destroy: () => form.removeEventListener('keydown', keydown),
|
destroy: () => {
|
||||||
|
form.removeEventListener('keydown', keydown)
|
||||||
|
form.removeEventListener('paste', paste)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if session.signedIn}
|
{#if session.signedIn}
|
||||||
<form class="composer" onsubmit={submit} use:composerShortcuts>
|
<form class="composer" onsubmit={submit} use:composerInteractions>
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error-note" role="alert">{error}</p>
|
<p class="error-note" role="alert">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -102,4 +102,81 @@ describe('Composer', () => {
|
|||||||
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
|
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
|
||||||
expect(postStatus.mock.calls[0][1].status).toBe('Keyboard-posted entry')
|
expect(postStatus.mock.calls[0][1].status).toBe('Keyboard-posted entry')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uploads a pasted clipboard image and attaches it to the post', async () => {
|
||||||
|
const pastedImage = new File(['image bytes'], 'pasted-image.png', { type: 'image/png' })
|
||||||
|
const uploadMedia = vi.fn().mockResolvedValue({
|
||||||
|
id: 'pasted-media',
|
||||||
|
type: 'image',
|
||||||
|
url: 'https://media.example/pasted-image.png',
|
||||||
|
preview_url: 'https://media.example/pasted-image-preview.png',
|
||||||
|
description: null,
|
||||||
|
})
|
||||||
|
const postStatus = vi.fn().mockResolvedValue(status())
|
||||||
|
const services = testServices({
|
||||||
|
session: session({
|
||||||
|
token: 'token',
|
||||||
|
me: account(),
|
||||||
|
signedIn: true,
|
||||||
|
instance: {
|
||||||
|
title: 'Test server',
|
||||||
|
version: '1.0.0',
|
||||||
|
configuration: { statuses: { max_media_attachments: 4 } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
endpoints: { uploadMedia, postStatus },
|
||||||
|
})
|
||||||
|
const view = render(Composer, {
|
||||||
|
props: { initialText: 'A pasted picture' },
|
||||||
|
context: new Map([[APP_SERVICES, services]]),
|
||||||
|
})
|
||||||
|
const textarea = view.getByRole('textbox', { name: 'Entry text' })
|
||||||
|
|
||||||
|
await fireEvent.paste(textarea, {
|
||||||
|
clipboardData: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
kind: 'file',
|
||||||
|
type: 'image/png',
|
||||||
|
getAsFile: () => pastedImage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
files: [pastedImage],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => expect(uploadMedia).toHaveBeenCalledOnce())
|
||||||
|
expect(uploadMedia.mock.calls[0][1]).toBe(pastedImage)
|
||||||
|
expect(view.container.querySelector('.composer-attachment img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'https://media.example/pasted-image-preview.png',
|
||||||
|
)
|
||||||
|
|
||||||
|
await fireEvent.click(view.getByRole('button', { name: 'Post Entry' }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
|
||||||
|
expect(postStatus.mock.calls[0][1].media_ids).toEqual(['pasted-media'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves ordinary text-only paste alone', async () => {
|
||||||
|
const uploadMedia = vi.fn()
|
||||||
|
const services = testServices({
|
||||||
|
session: session({ token: 'token', me: account(), signedIn: true }),
|
||||||
|
endpoints: { uploadMedia },
|
||||||
|
})
|
||||||
|
const view = render(Composer, {
|
||||||
|
context: new Map([[APP_SERVICES, services]]),
|
||||||
|
})
|
||||||
|
const textarea = view.getByRole('textbox', { name: 'Entry text' })
|
||||||
|
|
||||||
|
const wasNotCancelled = await fireEvent.paste(textarea, {
|
||||||
|
clipboardData: {
|
||||||
|
items: [{ kind: 'string', type: 'text/plain', getAsFile: () => null }],
|
||||||
|
files: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wasNotCancelled).toBe(true)
|
||||||
|
expect(uploadMedia).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user