diff --git a/.gitignore b/.gitignore
index 0c7b40f..407303d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,6 @@ logs
!.env.example
config
-public/emojis
\ No newline at end of file
+public/emojis
+
+.npmrc
diff --git a/bun.lockb b/bun.lockb
index c5be77a..0659575 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/composer/composer.vue b/components/composer/composer.vue
index dfe46cf..1387a3c 100644
--- a/components/composer/composer.vue
+++ b/components/composer/composer.vue
@@ -6,9 +6,9 @@
-
+
@@ -28,8 +28,11 @@
- state.contentType = i ? 'text/plain' : 'text/markdown'">
+
+ (state.contentType = i
+ ? 'text/html'
+ : 'text/plain')
+ ">
@@ -87,7 +90,11 @@
@@ -112,9 +119,9 @@ import Note from "~/components/notes/note.vue";
import { Select, SelectContent, SelectItem } from "~/components/ui/select";
import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
+import EditorContent from "../editor/content.vue";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
-import { Textarea } from "../ui/textarea";
import { Toggle } from "../ui/toggle";
import Files from "./files.vue";
@@ -124,13 +131,6 @@ const defaultVisibility = useSetting(SettingIds.DefaultVisibility);
const { play } = useAudio();
const fileInput = ref
(null);
-onMounted(() => {
- // Wait 0.3s for the dialog to open
- setTimeout(() => {
- document.getElementById("text-input")?.focus();
- }, 300);
-});
-
watch([Control_Enter, Command_Enter], () => {
if (sending.value || !ctrlEnterSend.value.value) {
return;
@@ -176,7 +176,7 @@ const state = reactive({
content: relation?.source?.text || getMentions(),
sensitive: relation?.type === "edit" ? relation.note.sensitive : false,
contentWarning: relation?.type === "edit" ? relation.note.spoiler_text : "",
- contentType: "text/markdown" as "text/markdown" | "text/plain",
+ contentType: "text/html" as "text/html" | "text/plain",
visibility: (relation?.type === "edit"
? relation.note.visibility
: (defaultVisibility.value.value ?? "public")) as Status["visibility"],
diff --git a/components/editor/content.vue b/components/editor/content.vue
new file mode 100644
index 0000000..674298e
--- /dev/null
+++ b/components/editor/content.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/editor/mentions-list.vue b/components/editor/mentions-list.vue
new file mode 100644
index 0000000..9eef674
--- /dev/null
+++ b/components/editor/mentions-list.vue
@@ -0,0 +1,80 @@
+
+
+
+ No results found.
+
+
+
+ {{ user.value.display_name }}
+
+
+
+
+
+
+
diff --git a/components/editor/suggestion.ts b/components/editor/suggestion.ts
new file mode 100644
index 0000000..66334b4
--- /dev/null
+++ b/components/editor/suggestion.ts
@@ -0,0 +1,99 @@
+import { VueRenderer } from "@tiptap/vue-3";
+import tippy, { type Instance } from "tippy.js";
+
+import type { MentionNodeAttrs } from "@tiptap/extension-mention";
+import type { SuggestionOptions } from "@tiptap/suggestion";
+import type { Account } from "@versia/client/types";
+import { go } from "fuzzysort";
+import MentionList from "./mentions-list.vue";
+
+export type UserData = {
+ key: string;
+ value: Account;
+};
+
+export default {
+ items: async ({ query }) => {
+ if (query.length === 0) {
+ return [];
+ }
+
+ const users = await client.value.searchAccount(query, { limit: 20 });
+
+ return go(
+ query,
+ users.data
+ // Deduplicate users
+ .filter(
+ (user, index, self) =>
+ self.findIndex((u) => u.acct === user.acct) === index,
+ )
+ .map((user) => ({
+ key: user.acct,
+ value: user,
+ })),
+ { key: "key" },
+ )
+ .map((result) => ({
+ key: result.obj.key,
+ value: result.obj.value,
+ }))
+ .slice(0, 20);
+ },
+
+ render: () => {
+ let component: VueRenderer;
+ let popup: Instance[] & Instance;
+
+ return {
+ onStart: (props) => {
+ component = new VueRenderer(MentionList, {
+ props,
+ editor: props.editor,
+ });
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ // @ts-expect-error Tippy types are wrong
+ popup = tippy("body", {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: "manual",
+ placement: "bottom-start",
+ });
+ },
+
+ onUpdate(props) {
+ component.updateProps(props);
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup[0]?.setProps({
+ getReferenceClientRect: props.clientRect as () => DOMRect,
+ });
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === "Escape") {
+ popup[0]?.hide();
+
+ return true;
+ }
+
+ return component.ref?.onKeyDown(props);
+ },
+
+ onExit() {
+ popup[0]?.destroy();
+ component.destroy();
+ },
+ };
+ },
+} as Omit, "editor">;
diff --git a/components/notes/content.vue b/components/notes/content.vue
index 4e7ead5..6422d43 100644
--- a/components/notes/content.vue
+++ b/components/notes/content.vue
@@ -82,62 +82,5 @@ const formattedCharacterCount = characterCount
diff --git a/components/profiles/profile-content.vue b/components/profiles/profile-content.vue
index a0d42ba..0900dd5 100644
--- a/components/profiles/profile-content.vue
+++ b/components/profiles/profile-content.vue
@@ -13,60 +13,5 @@ const { content } = defineProps<{
\ No newline at end of file
+@import url("~/styles/content.css");
+
diff --git a/components/ui/command/Command.vue b/components/ui/command/Command.vue
new file mode 100644
index 0000000..08ca2ab
--- /dev/null
+++ b/components/ui/command/Command.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandDialog.vue b/components/ui/command/CommandDialog.vue
new file mode 100644
index 0000000..5e30633
--- /dev/null
+++ b/components/ui/command/CommandDialog.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/components/ui/command/CommandEmpty.vue b/components/ui/command/CommandEmpty.vue
new file mode 100644
index 0000000..15a265a
--- /dev/null
+++ b/components/ui/command/CommandEmpty.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandGroup.vue b/components/ui/command/CommandGroup.vue
new file mode 100644
index 0000000..1863ea1
--- /dev/null
+++ b/components/ui/command/CommandGroup.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+ {{ heading }}
+
+
+
+
diff --git a/components/ui/command/CommandInput.vue b/components/ui/command/CommandInput.vue
new file mode 100644
index 0000000..22b12b6
--- /dev/null
+++ b/components/ui/command/CommandInput.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandItem.vue b/components/ui/command/CommandItem.vue
new file mode 100644
index 0000000..d7bce6c
--- /dev/null
+++ b/components/ui/command/CommandItem.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandList.vue b/components/ui/command/CommandList.vue
new file mode 100644
index 0000000..648d5a2
--- /dev/null
+++ b/components/ui/command/CommandList.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandSeparator.vue b/components/ui/command/CommandSeparator.vue
new file mode 100644
index 0000000..ff50e9a
--- /dev/null
+++ b/components/ui/command/CommandSeparator.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/components/ui/command/CommandShortcut.vue b/components/ui/command/CommandShortcut.vue
new file mode 100644
index 0000000..52b7f01
--- /dev/null
+++ b/components/ui/command/CommandShortcut.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/components/ui/command/index.ts b/components/ui/command/index.ts
new file mode 100644
index 0000000..2f33b76
--- /dev/null
+++ b/components/ui/command/index.ts
@@ -0,0 +1,9 @@
+export { default as Command } from "./Command.vue";
+export { default as CommandDialog } from "./CommandDialog.vue";
+export { default as CommandEmpty } from "./CommandEmpty.vue";
+export { default as CommandGroup } from "./CommandGroup.vue";
+export { default as CommandInput } from "./CommandInput.vue";
+export { default as CommandItem } from "./CommandItem.vue";
+export { default as CommandList } from "./CommandList.vue";
+export { default as CommandSeparator } from "./CommandSeparator.vue";
+export { default as CommandShortcut } from "./CommandShortcut.vue";
diff --git a/messages/en.json b/messages/en.json
index 68759fa..243a211 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -157,7 +157,7 @@
"mean_mean_badger_inspire": "Value",
"antsy_whole_alligator_blink": "Confirm",
"game_tough_seal_adore": "Mention someone",
- "plane_born_koala_hope": "Enable Markdown",
+ "plane_born_koala_hope": "Enable rich text",
"blue_ornate_coyote_tickle": "Insert emoji",
"top_patchy_earthworm_vent": "Attach a file",
"frail_broad_mallard_dart": "Mark as sensitive",
diff --git a/messages/fr.json b/messages/fr.json
index b87cb5d..1017cc2 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -147,7 +147,7 @@
"mean_mean_badger_inspire": "Valeur",
"antsy_whole_alligator_blink": "Confirmer",
"game_tough_seal_adore": "Mentionner quelqu'un",
- "plane_born_koala_hope": "Activer le Markdown",
+ "plane_born_koala_hope": "Activer le formatage du texte",
"blue_ornate_coyote_tickle": "Insérer un emoji",
"top_patchy_earthworm_vent": "Joindre un fichier",
"frail_broad_mallard_dart": "Marquer comme sensible",
diff --git a/package.json b/package.json
index 79845f6..e5fc229 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
},
"scripts": {
"build": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt build",
- "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bun --bun nuxt dev --https --https.cert config/versia-fe.localhost.pem --https.key config/versia-fe.localhost-key.pem --host versia-fe.localhost",
+ "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bun nuxt dev --https --https.cert config/versia-fe.localhost.pem --https.key config/versia-fe.localhost-key.pem --host versia-fe.localhost",
"generate": "nuxt generate",
"emojis:generate": "bun run utils/emojis.ts",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt prepare",
@@ -33,6 +33,20 @@
"@nuxt/fonts": "^0.10.3",
"@nuxtjs/color-mode": "3.5.2",
"@tailwindcss/typography": "^0.5.15",
+ "@tiptap/extension-highlight": "^2.10.4",
+ "@tiptap/extension-image": "^2.10.4",
+ "@tiptap/extension-link": "^2.10.4",
+ "@tiptap/extension-mention": "^2.10.4",
+ "@tiptap/extension-placeholder": "^2.10.4",
+ "@tiptap/extension-subscript": "^2.10.4",
+ "@tiptap/extension-superscript": "^2.10.4",
+ "@tiptap/extension-task-item": "^2.10.4",
+ "@tiptap/extension-task-list": "^2.10.4",
+ "@tiptap/extension-underline": "^2.10.4",
+ "@tiptap/pm": "^2.10.4",
+ "@tiptap/starter-kit": "^2.10.4",
+ "@tiptap/suggestion": "^2.10.4",
+ "@tiptap/vue-3": "^2.10.4",
"@vee-validate/zod": "^4.15.0",
"@versia/client": "0.1.4",
"@vite-pwa/nuxt": "^0.10.6",
@@ -41,7 +55,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.5.1",
- "fastest-levenshtein": "^1.0.16",
+ "fuzzysort": "^3.1.0",
"html-to-text": "^9.0.5",
"lucide-vue-next": "^0.469.0",
"magic-regexp": "^0.8.0",
diff --git a/styles/content.css b/styles/content.css
new file mode 100644
index 0000000..5e18e83
--- /dev/null
+++ b/styles/content.css
@@ -0,0 +1,15 @@
+.content code:not(pre code)::after,
+.content code:not(pre code)::before {
+ content: "";
+}
+
+.content ol li input[type="checkbox"],
+.content ul li input[type="checkbox"] {
+ border-radius: 0.25rem;
+ margin-bottom: 0.2rem;
+ margin-right: 0.5rem;
+ margin-top: 0;
+ vertical-align: middle;
+ --tw-text-opacity: 1;
+ color: var(--theme-primary-400);
+}