2024-04-27 09:04:02 +02:00
|
|
|
<template>
|
2025-05-27 21:45:54 +02:00
|
|
|
<div v-if="relation" class="overflow-auto max-h-72">
|
2026-01-09 21:47:12 +01:00
|
|
|
<Note :note="relation.note" :hide-actions="true" :small-layout="true" />
|
2024-12-01 18:29:54 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-09 22:12:23 +01:00
|
|
|
<InputGroup class="p-1">
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupAddon
|
|
|
|
|
v-if="store.sensitive"
|
|
|
|
|
align="block-start"
|
|
|
|
|
class="pt-3"
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
v-model:model-value="store.contentWarning"
|
|
|
|
|
placeholder="Put your content warning here"
|
|
|
|
|
/>
|
2025-12-09 22:12:23 +01:00
|
|
|
</InputGroupAddon>
|
2024-12-01 15:30:42 +01:00
|
|
|
|
2025-12-09 22:32:22 +01:00
|
|
|
<EditorContent
|
|
|
|
|
data-slot="input-group-control"
|
|
|
|
|
@paste-files="uploadFiles"
|
|
|
|
|
v-model:content="store.content"
|
|
|
|
|
v-model:raw-content="store.rawContent"
|
|
|
|
|
:placeholder="getRandomSplash()"
|
2025-12-09 22:12:23 +01:00
|
|
|
class=" placeholder:text-muted-foreground flex field-sizing-content min-h-58 w-full px-4 text-base disabled:opacity-50 md:text-sm flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none"
|
2025-12-09 22:32:22 +01:00
|
|
|
:disabled="store.sending"
|
|
|
|
|
:mode="store.contentType === 'text/html' ? 'rich' : 'plain'"
|
|
|
|
|
/>
|
2025-12-09 22:12:23 +01:00
|
|
|
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupAddon
|
|
|
|
|
v-if="store.files.length > 0"
|
|
|
|
|
align="block-end"
|
|
|
|
|
class="overflow-x-auto *:shrink-0"
|
|
|
|
|
>
|
2026-01-09 21:47:12 +01:00
|
|
|
<Files v-model:files="store.files" :composer-key="composerKey" />
|
2025-12-09 22:12:23 +01:00
|
|
|
</InputGroupAddon>
|
|
|
|
|
|
|
|
|
|
<InputGroupAddon align="block-end">
|
|
|
|
|
<Select v-model:model-value="store.contentType">
|
2025-12-09 22:32:22 +01:00
|
|
|
<SelectTrigger
|
|
|
|
|
as-child
|
|
|
|
|
disable-default-classes
|
|
|
|
|
disable-select-icon
|
|
|
|
|
>
|
2025-12-09 22:12:23 +01:00
|
|
|
<InputGroupButton variant="ghost" size="icon-sm">
|
2026-01-09 21:47:12 +01:00
|
|
|
<LetterText v-if="store.contentType === 'text/html'" />
|
|
|
|
|
<Type v-else />
|
2025-12-09 22:12:23 +01:00
|
|
|
</InputGroupButton>
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2025-12-09 22:32:22 +01:00
|
|
|
<SelectItem value="text/plain">Plain text</SelectItem>
|
|
|
|
|
<SelectItem value="text/html">Rich text</SelectItem>
|
2025-12-09 22:12:23 +01:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<VisibilityPicker v-model:visibility="store.visibility">
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
:disabled="store.relation?.type === 'edit'"
|
|
|
|
|
>
|
2026-01-09 21:47:12 +01:00
|
|
|
<component :is="visibilities[store.visibility].icon" />
|
2025-12-09 22:12:23 +01:00
|
|
|
</InputGroupButton>
|
|
|
|
|
</VisibilityPicker>
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
@click="fileInput?.click()"
|
|
|
|
|
>
|
2026-01-09 21:47:12 +01:00
|
|
|
<FilePlus2 />
|
2025-12-09 22:12:23 +01:00
|
|
|
</InputGroupButton>
|
|
|
|
|
<Toggle size="sm" v-model="store.sensitive">
|
2026-01-09 21:47:12 +01:00
|
|
|
<TriangleAlert />
|
2025-12-09 22:12:23 +01:00
|
|
|
</Toggle>
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupText
|
|
|
|
|
:class="['ml-auto', charactersLeft < 0 && 'text-destructive']"
|
|
|
|
|
>
|
2025-12-09 22:12:23 +01:00
|
|
|
{{ charactersLeft.toLocaleString(getLocale(), {
|
|
|
|
|
maximumFractionDigits: 2,
|
|
|
|
|
notation: 'compact',
|
|
|
|
|
compactDisplay: 'short',
|
|
|
|
|
}) }}
|
|
|
|
|
</InputGroupText>
|
2026-01-09 21:47:12 +01:00
|
|
|
<Separator orientation="vertical" class="h-4!" />
|
2025-12-09 22:32:22 +01:00
|
|
|
<InputGroupButton
|
|
|
|
|
variant="default"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
:disabled="store.sending || !store.canSend"
|
|
|
|
|
@click="send"
|
|
|
|
|
>
|
2026-01-09 21:47:12 +01:00
|
|
|
<Spinner v-if="store.sending" />
|
|
|
|
|
<ArrowUp v-else />
|
2025-12-09 22:12:23 +01:00
|
|
|
<span class="sr-only">Send</span>
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
</InputGroupAddon>
|
|
|
|
|
</InputGroup>
|
2024-12-01 17:20:21 +01:00
|
|
|
|
2025-12-09 22:32:22 +01:00
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
ref="fileInput"
|
|
|
|
|
@change="uploadFileFromEvent"
|
|
|
|
|
class="hidden"
|
|
|
|
|
multiple
|
|
|
|
|
>
|
2024-04-27 09:04:02 +02:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2025-12-09 22:12:23 +01:00
|
|
|
import {
|
|
|
|
|
ArrowUp,
|
|
|
|
|
FilePlus2,
|
|
|
|
|
LetterText,
|
|
|
|
|
TriangleAlert,
|
|
|
|
|
Type,
|
|
|
|
|
} from "lucide-vue-next";
|
|
|
|
|
import { Separator } from "reka-ui";
|
2024-12-01 18:29:54 +01:00
|
|
|
import Note from "~/components/notes/note.vue";
|
2025-12-09 22:12:23 +01:00
|
|
|
import { getLocale } from "~~/paraglide/runtime";
|
2024-12-25 20:46:14 +01:00
|
|
|
import EditorContent from "../editor/content.vue";
|
2025-12-09 22:12:23 +01:00
|
|
|
import { Input } from "../ui/input";
|
|
|
|
|
import {
|
|
|
|
|
InputGroup,
|
|
|
|
|
InputGroupAddon,
|
|
|
|
|
InputGroupButton,
|
|
|
|
|
InputGroupText,
|
|
|
|
|
} from "../ui/input-group";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
|
|
|
|
|
import { Spinner } from "../ui/spinner";
|
|
|
|
|
import { Toggle } from "../ui/toggle";
|
2025-03-28 01:16:24 +01:00
|
|
|
import Files from "./files.vue";
|
2025-12-09 22:12:23 +01:00
|
|
|
import { visibilities } from "./visibilities";
|
|
|
|
|
import VisibilityPicker from "./visibility-picker.vue";
|
2024-12-01 15:30:42 +01:00
|
|
|
|
2025-08-28 07:41:51 +02:00
|
|
|
const props = defineProps<{
|
|
|
|
|
relation?: ComposerState["relation"];
|
|
|
|
|
}>();
|
|
|
|
|
|
2024-12-01 15:30:42 +01:00
|
|
|
const { Control_Enter, Command_Enter } = useMagicKeys();
|
2025-08-28 07:41:51 +02:00
|
|
|
const { play } = useAudio();
|
2025-06-27 00:28:14 +02:00
|
|
|
const fileInput = useTemplateRef<HTMLInputElement>("fileInput");
|
2025-08-28 07:41:51 +02:00
|
|
|
const composerKey = props.relation
|
|
|
|
|
? (`${props.relation.type}-${props.relation.note.id}` as const)
|
|
|
|
|
: "blank";
|
|
|
|
|
const store = useComposerStore(composerKey)();
|
2025-12-09 22:12:23 +01:00
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
const charactersLeft = computed(() => {
|
|
|
|
|
const max = authStore.instance?.configuration.statuses.max_characters ?? 0;
|
|
|
|
|
|
|
|
|
|
return max - store.rawContent.length;
|
|
|
|
|
});
|
2024-11-30 19:15:23 +01:00
|
|
|
|
2024-12-01 15:30:42 +01:00
|
|
|
watch([Control_Enter, Command_Enter], () => {
|
2025-08-28 07:41:51 +02:00
|
|
|
if (store.sending || !preferences.ctrl_enter_send.value) {
|
2024-12-01 15:30:42 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 00:28:14 +02:00
|
|
|
send();
|
2024-12-01 15:30:42 +01:00
|
|
|
});
|
|
|
|
|
|
2025-08-28 07:41:51 +02:00
|
|
|
const getRandomSplash = (): string => {
|
|
|
|
|
const splashes = useConfig().COMPOSER_SPLASHES;
|
|
|
|
|
|
|
|
|
|
return splashes[Math.floor(Math.random() * splashes.length)] as string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const send = async () => {
|
|
|
|
|
const result =
|
|
|
|
|
store.relation?.type === "edit"
|
|
|
|
|
? await store.sendEdit()
|
|
|
|
|
: await store.send();
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
play("publish");
|
|
|
|
|
store.$reset();
|
|
|
|
|
useEvent("composer:close");
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-04-27 09:04:02 +02:00
|
|
|
|
2025-06-27 00:28:14 +02:00
|
|
|
watch(
|
|
|
|
|
props,
|
|
|
|
|
async (props) => {
|
2025-08-28 08:08:14 +02:00
|
|
|
if (props.relation && !store.relation) {
|
2025-08-28 07:41:51 +02:00
|
|
|
store.stateFromRelation(
|
2025-06-27 00:28:14 +02:00
|
|
|
props.relation.type,
|
|
|
|
|
props.relation.note,
|
|
|
|
|
props.relation.source,
|
|
|
|
|
);
|
2024-12-02 10:29:03 +01:00
|
|
|
}
|
2025-06-27 00:28:14 +02:00
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
);
|
2024-12-01 15:30:42 +01:00
|
|
|
|
2024-12-01 17:20:21 +01:00
|
|
|
const uploadFileFromEvent = (e: Event) => {
|
|
|
|
|
const target = e.target as HTMLInputElement;
|
|
|
|
|
const files = Array.from(target.files ?? []);
|
|
|
|
|
|
2025-06-27 00:28:14 +02:00
|
|
|
for (const file of files) {
|
2025-08-28 07:41:51 +02:00
|
|
|
store.uploadFile(file);
|
2025-06-27 00:28:14 +02:00
|
|
|
}
|
2024-12-01 17:20:21 +01:00
|
|
|
|
|
|
|
|
target.value = "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const uploadFiles = (files: File[]) => {
|
|
|
|
|
for (const file of files) {
|
2025-08-28 07:41:51 +02:00
|
|
|
store.uploadFile(file);
|
2024-12-01 17:20:21 +01:00
|
|
|
}
|
|
|
|
|
};
|
2024-12-17 12:30:01 +01:00
|
|
|
</script>
|