1
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 >
2
+ <RespondingTo v-if =" respondingTo" :respondingTo =" respondingTo" />
7
3
<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" />
44
19
</div >
45
20
</template >
46
21
47
22
<script lang="ts" setup>
48
23
import type { Instance , Status } from " @versia/client/types" ;
49
24
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" ;
57
34
58
35
const uploader = ref <InstanceType <typeof FileUploader > | undefined >(undefined );
59
36
const { Control_Enter, Command_Enter, Control_Alt } = useMagicKeys ();
@@ -65,7 +42,9 @@ const cwContent = ref("");
65
42
const markdown = ref (true );
66
43
67
44
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
+ );
69
48
70
49
const openFilePicker = () => {
71
50
uploader .value ?.openFilePicker ();
@@ -95,13 +74,14 @@ const handlePaste = (event: ClipboardEvent) => {
95
74
};
96
75
97
76
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 ;
99
80
});
100
81
101
82
watch (
102
83
files ,
103
84
(newFiles ) => {
104
- // If a file is uploading, set loading to true
105
85
loading .value = newFiles .some ((file ) => file .uploading );
106
86
},
107
87
{
@@ -137,7 +117,6 @@ onMounted(() => {
137
117
alt_text: file .description ?? undefined ,
138
118
}));
139
119
140
- // Fetch source
141
120
const source = await client .value .getStatusSource (note .id );
142
121
143
122
if (source ?.data ) {
@@ -169,61 +148,72 @@ const canSubmit = computed(
169
148
);
170
149
171
150
const send = async () => {
172
- loading .value = true ;
173
151
if (! (identity .value && client .value )) {
174
152
throw new Error (" Not authenticated" );
175
153
}
176
154
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
+ );
187
204
188
205
if (! response .data ) {
189
- throw new Error (" Failed to edit status" );
206
+ throw new Error (" Failed to send status" );
190
207
}
191
208
192
209
content .value = " " ;
193
210
loading .value = false ;
194
- useEvent (" composer:send-edit " , response .data );
211
+ useEvent (" composer:send" , response .data as Status );
195
212
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 ;
221
216
}
222
-
223
- content .value = " " ;
224
- loading .value = false ;
225
- useEvent (" composer:send" , response .data as Status );
226
- useEvent (" composer:close" );
227
217
};
228
218
229
219
const characterLimit = computed (
0 commit comments