frontend/app/components/composer/composer.vue

171 lines
5.9 KiB
Vue
Raw Normal View History

2024-04-27 09:04:02 +02:00
<template>
<div v-if="relation" class="overflow-auto max-h-72">
<Note :note="relation.note" :hide-actions="true" :small-layout="true" />
</div>
2025-03-27 22:20:04 +01:00
<InputGroup class="p-1">
<InputGroupAddon v-if="store.sensitive" align="block-start" class="pt-3">
<Input v-model:model-value="store.contentWarning" placeholder="Put your content warning here" />
</InputGroupAddon>
<EditorContent data-slot="input-group-control" @paste-files="uploadFiles" v-model:content="store.content"
v-model:raw-content="store.rawContent" :placeholder="getRandomSplash()"
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"
:disabled="store.sending" :mode="store.contentType === 'text/html' ? 'rich' : 'plain'" />
<InputGroupAddon v-if="store.files.length > 0" align="block-end" class="overflow-x-auto *:shrink-0">
<Files v-model:files="store.files" :composer-key="composerKey" />
</InputGroupAddon>
<InputGroupAddon align="block-end">
<Select v-model:model-value="store.contentType">
<SelectTrigger as-child disable-default-classes disable-select-icon>
<InputGroupButton variant="ghost" size="icon-sm">
<LetterText v-if="store.contentType === 'text/html'" />
<Type v-else />
</InputGroupButton>
</SelectTrigger>
<SelectContent>
<SelectItem value="text/plain">
Plain text
</SelectItem>
<SelectItem value="text/html">
Rich text
</SelectItem>
</SelectContent>
</Select>
<VisibilityPicker v-model:visibility="store.visibility">
<InputGroupButton variant="ghost" size="icon-sm" :disabled="store.relation?.type === 'edit'">
<component :is="visibilities[store.visibility].icon" />
</InputGroupButton>
</VisibilityPicker>
<InputGroupButton variant="ghost" size="icon-sm" @click="fileInput?.click()">
<FilePlus2 />
</InputGroupButton>
<Toggle size="sm" v-model="store.sensitive">
<TriangleAlert />
</Toggle>
<InputGroupText :class="['ml-auto', charactersLeft < 0 && 'text-destructive']">
{{ charactersLeft.toLocaleString(getLocale(), {
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
}) }}
</InputGroupText>
<Separator orientation="vertical" class="h-4!" />
<InputGroupButton variant="default" size="icon-sm" :disabled="store.sending || !store.canSend"
@click="send">
<Spinner v-if="store.sending" />
<ArrowUp v-else />
<span class="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<input type="file" ref="fileInput" @change="uploadFileFromEvent" class="hidden" multiple />
2024-04-27 09:04:02 +02:00
</template>
<script lang="ts" setup>
import {
ArrowUp,
FilePlus2,
LetterText,
TriangleAlert,
Type,
} from "lucide-vue-next";
import { Separator } from "reka-ui";
import Note from "~/components/notes/note.vue";
import { getLocale } from "~~/paraglide/runtime";
import EditorContent from "../editor/content.vue";
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";
import Files from "./files.vue";
import { visibilities } from "./visibilities";
import VisibilityPicker from "./visibility-picker.vue";
const props = defineProps<{
relation?: ComposerState["relation"];
}>();
const { Control_Enter, Command_Enter } = useMagicKeys();
const { play } = useAudio();
const fileInput = useTemplateRef<HTMLInputElement>("fileInput");
const composerKey = props.relation
? (`${props.relation.type}-${props.relation.note.id}` as const)
: "blank";
const store = useComposerStore(composerKey)();
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
watch([Control_Enter, Command_Enter], () => {
if (store.sending || !preferences.ctrl_enter_send.value) {
return;
}
send();
});
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
watch(
props,
async (props) => {
if (props.relation && !store.relation) {
store.stateFromRelation(
props.relation.type,
props.relation.note,
props.relation.source,
);
2024-12-02 10:29:03 +01:00
}
},
{ immediate: true },
);
const uploadFileFromEvent = (e: Event) => {
const target = e.target as HTMLInputElement;
const files = Array.from(target.files ?? []);
for (const file of files) {
store.uploadFile(file);
}
target.value = "";
};
const uploadFiles = (files: File[]) => {
for (const file of files) {
store.uploadFile(file);
}
};
</script>