Skip to content

Commit 343765a

Browse files
committed
refactor: ♻️ Refactor composer
1 parent 70222d1 commit 343765a

8 files changed

+231
-145
lines changed

bun.lockb

8 Bytes
Binary file not shown.
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<div class="flex flex-row gap-1 border-white/20">
3+
<Button title="Mention someone" @click="content = content + '@'">
4+
<iconify-icon height="1.5rem" width="1.5rem" icon="tabler:at" aria-hidden="true" />
5+
</Button>
6+
<Button title="Toggle Markdown" @click="markdown = !markdown" :toggled="markdown">
7+
<iconify-icon width="1.25rem" height="1.25rem"
8+
:icon="markdown ? 'tabler:markdown' : 'tabler:markdown-off'" aria-hidden="true" />
9+
</Button>
10+
<Button title="Use a custom emoji">
11+
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:mood-smile" aria-hidden="true" />
12+
</Button>
13+
<Button title="Add media" @click="emit('filePickerOpen')">
14+
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:photo-up" aria-hidden="true" />
15+
</Button>
16+
<Button title="Add a file" @click="emit('filePickerOpen')">
17+
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:file-upload" aria-hidden="true" />
18+
</Button>
19+
<Button title="Add content warning" @click="cw = !cw" :toggled="cw">
20+
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:rating-18-plus" aria-hidden="true" />
21+
</Button>
22+
<ButtonBase theme="primary" :loading="loading" @click="emit('send')" class="ml-auto rounded-full"
23+
:disabled="!canSubmit || loading">
24+
{{
25+
respondingType === "edit" ? "Edit!" : "Send!"
26+
}}
27+
</ButtonBase>
28+
</div>
29+
</template>
30+
31+
<script lang="ts" setup>
32+
import ButtonBase from "~/packages/ui/components/buttons/button.vue";
33+
import Button from "./button.vue";
34+
35+
defineProps<{
36+
loading: boolean;
37+
canSubmit: boolean;
38+
respondingType: string | null;
39+
}>();
40+
41+
const emit = defineEmits<{
42+
send: [];
43+
filePickerOpen: [];
44+
}>();
45+
46+
const cw = defineModel<boolean>("cw", {
47+
required: true,
48+
});
49+
const content = defineModel<string>("content", {
50+
required: true,
51+
});
52+
const markdown = defineModel<boolean>("markdown", {
53+
required: true,
54+
});
55+
</script>

components/composer/composer.vue

+85-95
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,36 @@
11
<template>
2-
<div v-if="respondingTo" class="mb-4" role="region" aria-label="Responding to">
3-
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
4-
<Note :element="respondingTo" :small="true" :disabled="true" class="!rounded-none !bg-primary-500/10" />
5-
</OverlayScrollbarsComponent>
6-
</div>
2+
<RespondingTo v-if="respondingTo" :respondingTo="respondingTo" />
73
<div class="px-6 pb-4 pt-5">
8-
<RichTextboxInput v-model:model-content="content" @paste="handlePaste" :disabled="loading"
9-
:placeholder="chosenSplash" :max-characters="characterLimit" class="focus:!ring-0 max-h-[70dvh]" />
10-
<!-- Content warning textbox -->
11-
<div v-if="cw" class="mb-4">
12-
<input type="text" v-model="cwContent" placeholder="Add a content warning"
13-
class="w-full p-2 mt-1 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none"
14-
aria-label="Content warning" />
15-
</div>
16-
<FileUploader v-model:files="files" ref="uploader" />
17-
<div class="flex flex-row gap-1 border-white/20">
18-
<Button title="Mention someone" @click="content = content + '@'">
19-
<iconify-icon height="1.5rem" width="1.5rem" icon="tabler:at" aria-hidden="true" />
20-
</Button>
21-
<Button title="Toggle Markdown" @click="markdown = !markdown" :toggled="markdown">
22-
<iconify-icon width="1.25rem" height="1.25rem"
23-
:icon="markdown ? 'tabler:markdown' : 'tabler:markdown-off'" aria-hidden="true" />
24-
</Button>
25-
<Button title="Use a custom emoji">
26-
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:mood-smile" aria-hidden="true" />
27-
</Button>
28-
<Button title="Add media" @click="openFilePicker">
29-
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:photo-up" aria-hidden="true" />
30-
</Button>
31-
<Button title="Add a file" @click="openFilePicker">
32-
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:file-upload" aria-hidden="true" />
33-
</Button>
34-
<Button title="Add content warning" @click="cw = !cw" :toggled="cw">
35-
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:rating-18-plus" aria-hidden="true" />
36-
</Button>
37-
<ButtonBase theme="primary" :loading="loading" @click="send" class="ml-auto rounded-full"
38-
:disabled="!canSubmit || loading">
39-
{{
40-
respondingType === "edit" ? "Edit!" : "Send!"
41-
}}
42-
</ButtonBase>
43-
</div>
4+
<RichTextbox v-model:content="content" :loading="loading" :chosenSplash="chosenSplash" :characterLimit="characterLimit"
5+
:handle-paste="handlePaste" />
6+
<ContentWarning v-model:cw="cw" v-model:cwContent="cwContent" />
7+
<FileUploader :files="files" ref="uploader" @add-file="(newFile) => {
8+
files.push(newFile);
9+
}" @change-file="(changedFile) => {
10+
const index = files.findIndex((file) => file.id === changedFile.id);
11+
if (index !== -1) {
12+
files[index] = changedFile;
13+
}
14+
}" @remove-file="(id) => {
15+
files.splice(files.findIndex((file) => file.id === id), 1);
16+
}" />
17+
<ActionButtons v-model:content="content" v-model:markdown="markdown" v-model:cw="cw" :loading="loading" :canSubmit="canSubmit"
18+
:respondingType="respondingType" @send="send" @file-picker-open="openFilePicker" />
4419
</div>
4520
</template>
4621

4722
<script lang="ts" setup>
4823
import type { Instance, Status } from "@versia/client/types";
4924
import { nanoid } from "nanoid";
50-
import ButtonBase from "~/packages/ui/components/buttons/button.vue";
51-
import { OverlayScrollbarsComponent } from "#imports";
52-
import RichTextboxInput from "../inputs/rich-textbox-input.vue";
53-
import Note from "../social-elements/notes/note.vue";
54-
import Button from "./button.vue";
55-
// biome-ignore lint/style/useImportType: Biome doesn't see the Vue code
56-
import FileUploader, { type FileData } from "./uploader/uploader.vue";
25+
import { computed, onMounted, ref, watch, watchEffect } from "vue";
26+
import { useConfig, useEvent, useListen, useMagicKeys } from "#imports";
27+
import ActionButtons from "./action-buttons.vue";
28+
import ContentWarning from "./content-warning.vue";
29+
import RespondingTo from "./responding-to.vue";
30+
import RichTextbox from "./rich-text-box.vue";
31+
// biome-ignore lint/style/useImportType: <explanation>
32+
import FileUploader from "./uploader/uploader.vue";
33+
import type { FileData } from "./uploader/uploader.vue";
5734
5835
const uploader = ref<InstanceType<typeof FileUploader> | undefined>(undefined);
5936
const { Control_Enter, Command_Enter, Control_Alt } = useMagicKeys();
@@ -65,7 +42,9 @@ const cwContent = ref("");
6542
const markdown = ref(true);
6643
6744
const splashes = useConfig().COMPOSER_SPLASHES;
68-
const chosenSplash = ref(splashes[Math.floor(Math.random() * splashes.length)]);
45+
const chosenSplash = ref(
46+
splashes[Math.floor(Math.random() * splashes.length)] as string,
47+
);
6948
7049
const openFilePicker = () => {
7150
uploader.value?.openFilePicker();
@@ -95,13 +74,14 @@ const handlePaste = (event: ClipboardEvent) => {
9574
};
9675
9776
watch(Control_Alt as ComputedRef<boolean>, () => {
98-
chosenSplash.value = splashes[Math.floor(Math.random() * splashes.length)];
77+
chosenSplash.value = splashes[
78+
Math.floor(Math.random() * splashes.length)
79+
] as string;
9980
});
10081
10182
watch(
10283
files,
10384
(newFiles) => {
104-
// If a file is uploading, set loading to true
10585
loading.value = newFiles.some((file) => file.uploading);
10686
},
10787
{
@@ -137,7 +117,6 @@ onMounted(() => {
137117
alt_text: file.description ?? undefined,
138118
}));
139119
140-
// Fetch source
141120
const source = await client.value.getStatusSource(note.id);
142121
143122
if (source?.data) {
@@ -169,61 +148,72 @@ const canSubmit = computed(
169148
);
170149
171150
const send = async () => {
172-
loading.value = true;
173151
if (!(identity.value && client.value)) {
174152
throw new Error("Not authenticated");
175153
}
176154
177-
if (respondingType.value === "edit" && respondingTo.value) {
178-
const response = await client.value.editStatus(respondingTo.value.id, {
179-
status: content.value?.trim() ?? "",
180-
content_type: markdown.value ? "text/markdown" : "text/plain",
181-
spoiler_text: cw.value ? cwContent.value.trim() : undefined,
182-
sensitive: cw.value,
183-
media_ids: files.value
184-
.filter((file) => !!file.api_id)
185-
.map((file) => file.api_id) as string[],
186-
});
155+
try {
156+
loading.value = true;
157+
158+
if (respondingType.value === "edit" && respondingTo.value) {
159+
const response = await client.value.editStatus(
160+
respondingTo.value.id,
161+
{
162+
status: content.value?.trim() ?? "",
163+
content_type: markdown.value
164+
? "text/markdown"
165+
: "text/plain",
166+
spoiler_text: cw.value ? cwContent.value.trim() : undefined,
167+
sensitive: cw.value,
168+
media_ids: files.value
169+
.filter((file) => !!file.api_id)
170+
.map((file) => file.api_id) as string[],
171+
},
172+
);
173+
174+
if (!response.data) {
175+
throw new Error("Failed to edit status");
176+
}
177+
178+
content.value = "";
179+
loading.value = false;
180+
useEvent("composer:send-edit", response.data);
181+
useEvent("composer:close");
182+
return;
183+
}
184+
185+
const response = await client.value.postStatus(
186+
content.value?.trim() ?? "",
187+
{
188+
content_type: markdown.value ? "text/markdown" : "text/plain",
189+
in_reply_to_id:
190+
respondingType.value === "reply"
191+
? respondingTo.value?.id
192+
: undefined,
193+
quote_id:
194+
respondingType.value === "quote"
195+
? respondingTo.value?.id
196+
: undefined,
197+
spoiler_text: cw.value ? cwContent.value.trim() : undefined,
198+
sensitive: cw.value,
199+
media_ids: files.value
200+
.filter((file) => !!file.api_id)
201+
.map((file) => file.api_id) as string[],
202+
},
203+
);
187204
188205
if (!response.data) {
189-
throw new Error("Failed to edit status");
206+
throw new Error("Failed to send status");
190207
}
191208
192209
content.value = "";
193210
loading.value = false;
194-
useEvent("composer:send-edit", response.data);
211+
useEvent("composer:send", response.data as Status);
195212
useEvent("composer:close");
196-
return;
197-
}
198-
199-
const response = await client.value.postStatus(
200-
content.value?.trim() ?? "",
201-
{
202-
content_type: markdown.value ? "text/markdown" : "text/plain",
203-
in_reply_to_id:
204-
respondingType.value === "reply"
205-
? respondingTo.value?.id
206-
: undefined,
207-
quote_id:
208-
respondingType.value === "quote"
209-
? respondingTo.value?.id
210-
: undefined,
211-
spoiler_text: cw.value ? cwContent.value.trim() : undefined,
212-
sensitive: cw.value,
213-
media_ids: files.value
214-
.filter((file) => !!file.api_id)
215-
.map((file) => file.api_id) as string[],
216-
},
217-
);
218-
219-
if (!response.data) {
220-
throw new Error("Failed to send status");
213+
} catch (error) {
214+
console.error(error);
215+
loading.value = false;
221216
}
222-
223-
content.value = "";
224-
loading.value = false;
225-
useEvent("composer:send", response.data as Status);
226-
useEvent("composer:close");
227217
};
228218
229219
const characterLimit = computed(
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<div v-if="cw" class="mb-4">
3+
<input type="text" v-model="cwContent" placeholder="Add a content warning"
4+
class="w-full p-2 mt-1 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none"
5+
aria-label="Content warning" />
6+
</div>
7+
</template>
8+
9+
<script lang="ts" setup>
10+
const cw = defineModel<boolean>("cw", {
11+
required: true,
12+
});
13+
const cwContent = defineModel<string>("cwContent", {
14+
required: true,
15+
});
16+
</script>

components/composer/responding-to.vue

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<div v-if="respondingTo" class="mb-4" role="region" aria-label="Responding to">
3+
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
4+
<Note :element="respondingTo" :small="true" :disabled="true" class="!rounded-none !bg-primary-500/10" />
5+
</OverlayScrollbarsComponent>
6+
</div>
7+
</template>
8+
9+
<script lang="ts" setup>
10+
import type { Status } from "@versia/client/types";
11+
import { OverlayScrollbarsComponent } from "#imports";
12+
import Note from "../social-elements/notes/note.vue";
13+
14+
const props = defineProps<{
15+
respondingTo: Status;
16+
}>();
17+
</script>

components/composer/rich-text-box.vue

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<RichTextboxInput v-model:model-content="content" @paste="handlePaste" :disabled="loading"
3+
:placeholder="chosenSplash" :max-characters="characterLimit" class="focus:!ring-0 max-h-[70dvh]" />
4+
</template>
5+
6+
<script lang="ts" setup>
7+
import RichTextboxInput from "../inputs/rich-textbox-input.vue";
8+
9+
defineProps<{
10+
loading: boolean;
11+
chosenSplash: string;
12+
characterLimit: number;
13+
handlePaste: (event: ClipboardEvent) => void;
14+
}>();
15+
16+
const content = defineModel<string>("content", {
17+
required: true,
18+
});
19+
</script>

0 commit comments

Comments
 (0)