chore: ⬆️ Upgrade to new @versia/client

This commit is contained in:
Jesse Wierzbinski 2025-05-26 11:19:15 +02:00
parent 0a157d06f6
commit f807b05784
No known key found for this signature in database
71 changed files with 451 additions and 492 deletions

View file

@ -24,7 +24,7 @@
"@tiptap/suggestion": "^2.12.0",
"@tiptap/vue-3": "^2.12.0",
"@vee-validate/zod": "^4.15.0",
"@versia/client": "0.1.5",
"@versia/client": "0.2.0-alpha.2",
"@videojs-player/vue": "^1.0.0",
"@vite-pwa/nuxt": "^1.0.1",
"@vueuse/core": "^13.2.0",
@ -270,7 +270,7 @@
"@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
"@badgateway/oauth2-client": ["@badgateway/oauth2-client@2.4.2", "", {}, "sha512-70Fmzlmn8EfCjjssls8N6E94quBUWnLhu4inPZU2pkwpc6ZvbErkLRvtkYl81KFCvVcuVC0X10QPZVNwjXo2KA=="],
"@badgateway/oauth2-client": ["@badgateway/oauth2-client@3.2.0", "", {}, "sha512-EHsoV6oLHot7HeYkIoSxCZApNgBjwNo1OTV9kXIDnmijGAshlVkJreVAAtexFn+sfDKPE0JW5SCPYJV1y4IoMg=="],
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
@ -780,7 +780,7 @@
"@vercel/nft": ["@vercel/nft@0.29.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-aVV0E6vJpuvImiMwU1/5QKkw2N96BRFE7mBYGS7FhXUoS6V7SarQ+8tuj33o7ofECz8JtHpmQ9JW+oVzOoB7MA=="],
"@versia/client": ["@versia/client@0.1.5", "", { "dependencies": { "@badgateway/oauth2-client": "^2.4.2", "zod": "^3.24.1" } }, "sha512-POD2/IT98EZZ32kWEPc3XUY2zApX94tuBftNWIMyoT04Sp7CPuvv1TT2fxM2kmgrC6kgbh4I6yirPpzVY+FpSA=="],
"@versia/client": ["@versia/client@0.2.0-alpha.2", "", { "dependencies": { "@badgateway/oauth2-client": "^3.0.0", "iso-639-1": "^3.1.5", "magic-regexp": "^0.10.0", "zod": "^3.24.2", "zod-openapi": "^4.2.4" } }, "sha512-/x1Z2tyJsfckCOLX8K8XDVs3rd3vX0wfCz20IMTc9uaGRkDTcL7MYzz20WCgOEW/WmnwL3iVf/jVgJm0NzoF4Q=="],
"@videojs-player/vue": ["@videojs-player/vue@1.0.0", "", { "peerDependencies": { "@types/video.js": "7.x", "video.js": "7.x", "vue": "3.x" } }, "sha512-WonTezRfKu3fYdQLt/ta+nuKH6gMZUv8l40Jke/j4Lae7IqeO/+lLAmBnh3ni88bwR+vkFXIlZ2Ci7VKInIYJg=="],
@ -1516,6 +1516,8 @@
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"iso-639-1": ["iso-639-1@3.1.5", "", {}, "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="],
@ -2444,6 +2446,8 @@
"zod": ["zod@3.25.23", "", {}, "sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg=="],
"zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],

View file

@ -138,7 +138,7 @@
<script lang="ts" setup>
import type { ResponseError } from "@versia/client";
import type { Status, StatusSource } from "@versia/client/types";
import type { Attachment, Status, StatusSource } from "@versia/client/schemas";
import {
AtSign,
FilePlus2,
@ -151,6 +151,7 @@ import {
TriangleAlert,
} from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import Note from "~/components/notes/note.vue";
import {
Select,
@ -182,8 +183,8 @@ watch([Control_Enter, Command_Enter], () => {
const { relation } = defineProps<{
relation?: {
type: "reply" | "quote" | "edit";
note: Status;
source?: StatusSource;
note: z.infer<typeof Status>;
source?: z.infer<typeof StatusSource>;
};
}>();
@ -279,7 +280,7 @@ const submit = async () => {
visibility: state.visibility,
});
useEvent("composer:send", data as Status);
useEvent("composer:send", data as z.infer<typeof Status>);
play("publish");
useEvent("composer:close");
}
@ -317,7 +318,9 @@ const uploadFiles = (files: File[]) => {
return;
}
state.files[index].apiId = media.data.id;
state.files[index].apiId = (
media.data as z.infer<typeof Attachment>
).id;
state.files[index].uploading = false;
})
.catch(() => {

View file

@ -5,8 +5,9 @@ import {
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
import type { Status, StatusSource } from "@versia/client/types";
import type { Status, StatusSource } from "@versia/client/schemas";
import { toast } from "vue-sonner";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import Composer from "./composer.vue";
@ -58,8 +59,8 @@ const open = ref(false);
const relation = ref(
null as {
type: "reply" | "quote" | "edit";
note: Status;
source?: StatusSource;
note: z.infer<typeof Status>;
source?: z.infer<typeof StatusSource>;
} | null,
);
</script>

View file

@ -1,15 +1,15 @@
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 { VueRenderer } from "@tiptap/vue-3";
import type { Account } from "@versia/client/schemas";
import { go } from "fuzzysort";
import tippy, { type Instance } from "tippy.js";
import type { z } from "zod";
import MentionList from "./mentions-list.vue";
export type UserData = {
key: string;
value: Account;
value: z.infer<typeof Account>;
};
export default {

View file

@ -17,8 +17,10 @@
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/schemas";
import { Ellipsis, Heart, Quote, Repeat, Reply } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime";
import { confirmModalService } from "../modals/composable";
@ -33,7 +35,7 @@ const { noteId } = defineProps<{
noteId: string;
isRemote: boolean;
url: string;
remoteUrl: string;
remoteUrl?: string;
authorId: string;
liked: boolean;
reblogged: boolean;
@ -108,7 +110,10 @@ const reblog = async () => {
const { data } = await client.value.reblogStatus(noteId);
toast.dismiss(id);
toast.success(m.weird_moving_hawk_lift());
useEvent("note:edit", data.reblog || data);
useEvent(
"note:edit",
(data.reblog as z.infer<typeof Status> | null) || data,
);
};
const unreblog = async () => {

View file

@ -6,13 +6,14 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import AudioAttachment from "./attachments/audio.vue";
import FileAttachment from "./attachments/file.vue";
import ImageAttachment from "./attachments/image.vue";
import VideoAttachment from "./attachments/video.vue";
defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
}>();
</script>

View file

@ -6,10 +6,11 @@
</template>
<script lang="ts" setup>
import type { Attachment as AttachmentType } from "@versia/client/types";
import type { Attachment as AttachmentType } from "@versia/client/schemas";
import type { z } from "zod";
import Attachment from "./attachment.vue";
defineProps<{
attachments: AttachmentType[];
attachments: z.infer<typeof AttachmentType>[];
}>();
</script>

View file

@ -5,10 +5,11 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
const { attachment } = defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
}>();
</script>

View file

@ -48,8 +48,9 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import { Captions, Download, File, X } from "lucide-vue-next";
import type { z } from "zod";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import {
@ -67,7 +68,7 @@ import {
} from "~/components/ui/popover";
const { attachment, lightbox = false } = defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
lightbox?: boolean;
}>();
</script>

View file

@ -8,11 +8,12 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import { File } from "lucide-vue-next";
import type { z } from "zod";
import Base from "./base.vue";
const { attachment } = defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
}>();
</script>

View file

@ -5,10 +5,11 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
const { attachment } = defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
}>();
</script>

View file

@ -5,10 +5,11 @@
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/types";
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
const { attachment } = defineProps<{
attachment: Attachment;
attachment: z.infer<typeof Attachment>;
}>();
</script>

View file

@ -13,7 +13,8 @@
</template>
<script lang="ts" setup>
import type { Attachment, Emoji, Status } from "@versia/client/types";
import type { Attachment, CustomEmoji, Status } from "@versia/client/schemas";
import type { z } from "zod";
import Attachments from "./attachments.vue";
import ContentWarning from "./content-warning.vue";
import Note from "./note.vue";
@ -23,9 +24,9 @@ import Prose from "./prose.vue";
const { content, plainContent, sensitive, contentWarning } = defineProps<{
plainContent?: string;
content: string;
quote?: NonNullable<Status["quote"]>;
emojis: Emoji[];
attachments: Attachment[];
quote?: NonNullable<z.infer<typeof Status.shape.quote>>;
emojis: z.infer<typeof CustomEmoji>[];
attachments: z.infer<typeof Attachment>[];
sensitive: boolean;
contentWarning?: string;
}>();

View file

@ -44,12 +44,13 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { Account, StatusVisibility } from "@versia/client/types";
import type { Account, Status } from "@versia/client/schemas";
import type {
UseTimeAgoMessages,
UseTimeAgoUnitNamesDefault,
} from "@vueuse/core";
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
import type { z } from "zod";
import { getLocale } from "~/paraglide/runtime";
import Avatar from "../profiles/avatar.vue";
import SmallCard from "../profiles/small-card.vue";
@ -61,11 +62,11 @@ import {
const { createdAt, noteUrl, author, authorUrl } = defineProps<{
cornerAvatar?: string;
visibility: StatusVisibility;
visibility: z.infer<typeof Status.shape.visibility>;
noteUrl: string;
createdAt: Date;
smallLayout?: boolean;
author: Account;
author: z.infer<typeof Account>;
authorUrl: string;
}>();

View file

@ -26,7 +26,7 @@ const { authorId, noteId } = defineProps<{
apiNoteString: string;
isRemote: boolean;
url: string;
remoteUrl: string;
remoteUrl?: string;
authorId: string;
noteId: string;
}>();
@ -102,7 +102,7 @@ const _delete = async () => {
<Link />
{{ m.ago_new_pelican_drip() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" v-if="isRemote" @click="copyText(remoteUrl)">
<DropdownMenuItem as="button" v-if="isRemote && remoteUrl" @click="copyText(remoteUrl)">
<Link />
{{ m.solid_witty_zebra_walk() }}
</DropdownMenuItem>

View file

@ -45,7 +45,7 @@
:content="noteToUse.content"
:quote="note.quote ?? undefined"
:attachments="noteToUse.media_attachments"
:plain-content="noteToUse.plain_content ?? undefined"
:plain-content="noteToUse.text ?? undefined"
:emojis="noteToUse.emojis"
:sensitive="noteToUse.sensitive"
:content-warning="noteToUse.spoiler_text"
@ -58,7 +58,7 @@
:url="url"
:api-note-string="JSON.stringify(noteToUse, null, 4)"
:reblog-count="noteToUse.reblogs_count"
:remote-url="noteToUse.url"
:remote-url="noteToUse.url ?? undefined"
:is-remote="isRemote"
:author-id="noteToUse.account.id"
@edit="useEvent('composer:edit', noteToUse)"
@ -75,15 +75,18 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
import Actions from "./actions.vue";
import Content from "./content.vue";
import Header from "./header.vue";
import ReblogHeader from "./reblog-header.vue";
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
const { note } = defineProps<{
note: Status;
note: PartialBy<z.infer<typeof Status>, "reblog" | "quote">;
hideActions?: boolean;
smallLayout?: boolean;
contentUnderUsername?: boolean;
@ -92,7 +95,11 @@ const { note } = defineProps<{
}>();
// Notes can be reblogs, in which case the actual thing to render is inside the reblog property
const noteToUse = computed(() => (note.reblog ? note.reblog : note));
const noteToUse = computed(() =>
note.reblog
? (note.reblog as z.infer<typeof Status>)
: (note as z.infer<typeof Status>),
);
const url = wrapUrl(`/@${noteToUse.value.account.acct}/${noteToUse.value.id}`);
const accountUrl = wrapUrl(`/@${noteToUse.value.account.acct}`);

View file

@ -10,8 +10,9 @@
</template>
<script lang="ts" setup>
import type { Emoji } from "@versia/client/types";
import type { CustomEmoji } from "@versia/client/schemas";
import { Repeat } from "lucide-vue-next";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import Avatar from "../profiles/avatar.vue";
import { Card } from "../ui/card";
@ -19,7 +20,7 @@ import { Card } from "../ui/card";
const { url } = defineProps<{
avatar: string;
displayName: string;
emojis: Emoji[];
emojis: z.infer<typeof CustomEmoji>[];
url: string;
}>();

View file

@ -17,11 +17,12 @@
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import Note from "./note.vue";
const { note } = defineProps<{
note: Status;
note: z.infer<typeof Status>;
}>();
const parent = useNote(client, note.in_reply_to_id);

View file

@ -35,16 +35,17 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import { Check, Loader, X } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import CopyableText from "~/components/notes/copyable-text.vue";
import { Button } from "~/components/ui/button";
import * as m from "~/paraglide/messages.js";
import Avatar from "../profiles/avatar.vue";
const { follower } = defineProps<{
follower: Account;
follower: z.infer<typeof Account>;
}>();
const loading = ref(true);

View file

@ -56,16 +56,16 @@
</template>
<script lang="ts" setup>
import type { Notification } from "@versia/client/types";
import type { Notification } from "@versia/client/schemas";
import {
AtSign,
ChevronDown,
Heart,
Repeat,
User,
UserCheck,
UserPlus,
} from "lucide-vue-next";
import type { z } from "zod";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import {
@ -84,7 +84,7 @@ import Avatar from "../profiles/avatar.vue";
import FollowRequest from "./follow-request.vue";
const { notification } = defineProps<{
notification: Notification;
notification: z.infer<typeof Notification>;
}>();
const icon = computed(() => {
@ -99,8 +99,8 @@ const icon = computed(() => {
return Heart;
case "follow_request":
return User;
case "follow_accept":
return UserCheck;
// case "follow_accept":
// return UserCheck;
default:
return null;
}
@ -118,8 +118,8 @@ const text = computed(() => {
return m.swift_just_beetle_devour();
case "follow_request":
return m.seemly_short_thrush_bloom();
case "follow_accept":
return m.weird_seemly_termite_scold();
//case "follow_accept":
// return m.weird_seemly_termite_scold();
default:
return "";
}

View file

@ -7,7 +7,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { toTypedSchema } from "@vee-validate/zod";
import type { Instance } from "@versia/client";
import type { Instance } from "@versia/client/schemas";
import { Loader } from "lucide-vue-next";
import { useForm } from "vee-validate";
import * as z from "zod";
@ -16,7 +16,7 @@ import { Input } from "~/components/ui/input";
import * as m from "~/paraglide/messages.js";
const { instance } = defineProps<{
instance: Instance;
instance: z.infer<typeof Instance>;
}>();
const isLoading = ref(false);

View file

@ -13,9 +13,10 @@
</template>
<script lang="ts" setup>
import { type Emoji, RolePermission } from "@versia/client/types";
import { type CustomEmoji, RolePermission } from "@versia/client/schemas";
import { Delete } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { confirmModalService } from "~/components/modals/composable";
import {
DropdownMenu,
@ -26,7 +27,7 @@ import {
import * as m from "~/paraglide/messages.js";
const { emojis } = defineProps<{
emojis: Emoji[];
emojis: z.infer<typeof CustomEmoji>[];
}>();
const permissions = usePermissions();

View file

@ -24,9 +24,10 @@
</template>
<script lang="ts" setup>
import { type Emoji, RolePermission } from "@versia/client/types";
import { type CustomEmoji, RolePermission } from "@versia/client/schemas";
import { Delete, MoreHorizontal, TextCursorInput } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { confirmModalService } from "~/components/modals/composable";
import { Button } from "~/components/ui/button";
import {
@ -38,7 +39,7 @@ import {
import * as m from "~/paraglide/messages.js";
const { emoji } = defineProps<{
emoji: Emoji;
emoji: z.infer<typeof CustomEmoji>;
}>();
const permissions = usePermissions();

View file

@ -5,8 +5,7 @@
</template>
<script lang="ts" setup>
import { type Emoji, RolePermission } from "@versia/client/types";
import * as m from "~/paraglide/messages.js";
import { RolePermission } from "@versia/client/schemas";
import Table from "./table.vue";
const permissions = usePermissions();
@ -16,36 +15,5 @@ const canUpload = computed(
permissions.value.includes(RolePermission.ManageEmojis),
);
const emojis = computed(
() =>
identity.value?.emojis?.filter((emoji) =>
emoji.shortcode.toLowerCase().includes(search.value.toLowerCase()),
) ?? [],
);
const search = ref("");
/**
* Sort emojis by category
*/
const categories = computed(() => {
const categories = new Map<string, Emoji[]>();
for (const emoji of emojis.value) {
if (!emoji.category) {
if (!categories.has(m.lucky_ago_rat_pinch())) {
categories.set(m.lucky_ago_rat_pinch(), []);
}
categories.get(m.lucky_ago_rat_pinch())?.push(emoji);
continue;
}
if (!categories.has(emoji.category)) {
categories.set(emoji.category, []);
}
categories.get(emoji.category)?.push(emoji);
}
return categories;
});
const emojis = computed(() => identity.value?.emojis ?? []);
</script>

View file

@ -33,7 +33,7 @@ import {
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table";
import type { Emoji } from "@versia/client/types";
import type { CustomEmoji } from "@versia/client/schemas";
import {
ArrowDownAZ,
ArrowUpAz,
@ -45,13 +45,14 @@ import {
Plus,
} from "lucide-vue-next";
import { ref } from "vue";
import type { z } from "zod";
import BatchDropdown from "./batch-dropdown.vue";
import Dropdown from "./dropdown.vue";
import Uploader from "./uploader.vue";
// No destructuring props to avoid reactivity issues
const props = defineProps<{
emojis: Emoji[];
emojis: z.infer<typeof CustomEmoji>[];
canUpload: boolean;
}>();
@ -64,7 +65,7 @@ const valueUpdater = <T extends Updater<any>>(updaterOrValue: T, ref: Ref) => {
: updaterOrValue;
};
const columns: ColumnDef<Emoji>[] = [
const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
{
id: "select",
header: ({ table }) => (

View file

@ -160,7 +160,7 @@
<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { RolePermission } from "@versia/client/types";
import { RolePermission } from "@versia/client/schemas";
import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import { z } from "zod";
@ -221,11 +221,11 @@ const formSchema = toTypedSchema(
.min(1)
.max(
identity.value?.instance.configuration.emojis
.max_emoji_shortcode_characters ?? Number.POSITIVE_INFINITY,
.max_shortcode_characters ?? Number.POSITIVE_INFINITY,
m.solid_inclusive_owl_hug({
count:
identity.value?.instance.configuration.emojis
.max_emoji_shortcode_characters ??
.max_shortcode_characters ??
Number.POSITIVE_INFINITY,
}),
)
@ -244,12 +244,11 @@ const formSchema = toTypedSchema(
.string()
.max(
identity.value?.instance.configuration.emojis
.max_emoji_description_characters ??
Number.POSITIVE_INFINITY,
.max_description_characters ?? Number.POSITIVE_INFINITY,
m.key_ago_hound_emerge({
count:
identity.value?.instance.configuration.emojis
.max_emoji_description_characters ??
.max_description_characters ??
Number.POSITIVE_INFINITY,
}),
)

View file

@ -13,10 +13,10 @@ export const formSchema = (identity: Identity) =>
(v) =>
v.size <=
(identity.instance.configuration.accounts
.header_size_limit ?? Number.POSITIVE_INFINITY),
.header_limit ?? Number.POSITIVE_INFINITY),
m.civil_icy_ant_mend({
size: identity.instance.configuration.accounts
.header_size_limit,
.header_limit,
}),
)
.optional(),
@ -26,10 +26,10 @@ export const formSchema = (identity: Identity) =>
(v) =>
v.size <=
(identity.instance.configuration.accounts
.avatar_size_limit ?? Number.POSITIVE_INFINITY),
.avatar_limit ?? Number.POSITIVE_INFINITY),
m.zippy_caring_raven_edit({
size: identity.instance.configuration.accounts
.avatar_size_limit,
.avatar_limit,
}),
)
.or(z.string().url())

View file

@ -71,7 +71,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import {
AtSign,
Ban,
@ -84,10 +84,11 @@ import {
VolumeX,
} from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
const { account } = defineProps<{
account: Account;
account: z.infer<typeof Account>;
}>();
const isMe = identity.value?.account.id === account.id;

View file

@ -25,12 +25,13 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import ProfileBadge from "./profile-badge.vue";
const { account } = defineProps<{
account: Account;
account: z.infer<typeof Account>;
}>();
const config = useConfig();

View file

@ -4,11 +4,12 @@
</template>
<script lang="ts" setup>
import type { Emoji } from "@versia/client/types";
import type { CustomEmoji } from "@versia/client/schemas";
import type { z } from "zod";
const { content } = defineProps<{
content: string;
emojis: Emoji[];
emojis: z.infer<typeof CustomEmoji>[];
}>();
</script>

View file

@ -8,10 +8,11 @@
</template>
<script lang="ts" setup>
import type { Emoji, Field } from "@versia/client/types";
import type { CustomEmoji, Field } from "@versia/client/schemas";
import type { z } from "zod";
defineProps<{
fields: Field[];
emojis: Emoji[];
fields: z.infer<typeof Field>[];
emojis: z.infer<typeof CustomEmoji>[];
}>();
</script>

View file

@ -66,9 +66,10 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import { Ellipsis, Loader } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import CopyableText from "~/components/notes/copyable-text.vue";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
@ -83,7 +84,7 @@ import ProfileHeader from "./profile-header.vue";
import ProfileStats from "./profile-stats.vue";
const { account } = defineProps<{
account: Account;
account: z.infer<typeof Account>;
}>();
const { relationship, isLoading } = useRelationship(client, account.id);

View file

@ -52,7 +52,8 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import { Separator } from "~/components/ui/separator";
import CopyableText from "../notes/copyable-text.vue";
import Avatar from "./avatar.vue";
@ -60,7 +61,7 @@ import ProfileContent from "./profile-content.vue";
import ProfileFields from "./profile-fields.vue";
const { account } = defineProps<{
account: Account;
account: z.infer<typeof Account>;
}>();
const [username, instance] = account.acct.split("@");

View file

@ -18,12 +18,13 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import { Card, CardContent } from "~/components/ui/card";
import Avatar from "./avatar.vue";
const { account, domain, naked } = defineProps<{
account: Account;
account: z.infer<typeof Account>;
domain: string;
naked?: boolean;
}>();

View file

@ -1,89 +0,0 @@
<template>
<!-- OIDC linked accounts manager -->
<div class="w-full ring-1 ring-white/5 pb-5 bg-dark-800 rounded overflow-hidden">
<div class="px-4 py-4">
<h3 class="font-semibold text-gray-300 text-xl">Linked accounts</h3>
</div>
<div class="px-4 py-3">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-4">
<div v-for="provider of ssoConfig?.providers" :key="provider.id"
class="flex items-center justify-between p-4 bg-dark-700 rounded">
<div class="flex items-center gap-4">
<Avatar :src="provider.icon" :alt="provider.name" class="size-8" />
<span class="font-semibold text-gray-300">{{ provider.name }}</span>
</div>
<div>
<Button theme="primary" :loading="loading"
v-if="!linkedProviders?.find(p => p.id === provider.id)" @click="link(provider.id)">
Link
</Button>
<Button theme="secondary" :loading="loading" v-else @click="unlink(provider.id)">
Unlink
</Button>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { ResponseError } from "@versia/client";
import Button from "~/packages/ui/components/buttons/button.vue";
import Avatar from "../avatars/avatar.vue";
const ssoConfig = useSSOConfig();
const linkedProviders = useLinkedSSO(client);
const loading = ref(false);
const link = async (providerId: string) => {
loading.value = true;
try {
const output = await client.value.post<{
link: string;
}>("/api/v1/sso", {
issuer: providerId,
});
window.location.href = output.data.link;
} catch (error) {
const e = error as ResponseError<{ error: string }>;
useEvent("notification:new", {
title: "Failed to link account",
description: e.response.data.error,
type: "error",
});
}
loading.value = false;
};
const unlink = async (providerId: string) => {
loading.value = true;
try {
await client.value.delete<void>(`/api/v1/sso/${providerId}`);
useEvent("notification:new", {
title: "Account unlinked",
type: "success",
});
linkedProviders.value = linkedProviders.value.filter(
(p) => p.id !== providerId,
);
} catch (error) {
const e = error as ResponseError<{ error: string }>;
useEvent("notification:new", {
title: "Failed to unlink account",
description: e.response.data.error,
type: "error",
});
}
loading.value = false;
};
</script>

View file

@ -44,8 +44,7 @@
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import { ChevronsUpDown, LogIn, LogOut, UserPlus } from "lucide-vue-next";
import { LogIn, LogOut, UserPlus } from "lucide-vue-next";
import { toast } from "vue-sonner";
import TinyCard from "~/components/profiles/tiny-card.vue";
import { Button } from "~/components/ui/button";

View file

@ -1,11 +1,11 @@
<template>
<Timeline type="status" :items="(items as Status[])" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import type { z } from "zod";
import Timeline from "./timeline.vue";
const props = defineProps<{

View file

@ -1,11 +1,11 @@
<template>
<Timeline type="status" :items="(items as Status[])" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import type { z } from "zod";
import { useGlobalTimeline } from "~/composables/GlobalTimeline";
import Timeline from "./timeline.vue";

View file

@ -1,13 +1,12 @@
<template>
<Timeline type="status" :items="(items as Status[])" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import { useHomeTimeline } from "~/composables/HomeTimeline";
<script lang="ts" setup>import { useHomeTimeline } from "~/composables/HomeTimeline";
import Timeline from "./timeline.vue";
import type { z } from "zod";
const {
error,

View file

@ -1,11 +1,11 @@
<template>
<Timeline type="status" :items="(items as Status[])" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import type { z } from "zod";
import { useLocalTimeline } from "~/composables/LocalTimeline";
import Timeline from "./timeline.vue";

View file

@ -1,11 +1,10 @@
<template>
<Timeline type="notification" :items="(items as Notification[])" :is-loading="isLoading"
<Timeline type="notification" :items="items" :is-loading="isLoading"
:has-reached-end="hasReachedEnd" :error="error" :load-next="loadNext" :load-prev="loadPrev"
:remove-item="removeItem" :update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Notification } from "@versia/client/types";
import { useNotificationTimeline } from "~/composables/NotificationTimeline";
import Timeline from "./timeline.vue";

View file

@ -1,11 +1,10 @@
<template>
<Timeline type="status" :items="(items as Status[])" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
</template>
<script lang="ts" setup>
import type { Status } from "@versia/client/types";
import { usePublicTimeline } from "~/composables/PublicTimeline";
import Timeline from "./timeline.vue";

View file

@ -1,15 +1,16 @@
<template>
<component :is="itemComponent" :note="type === 'status' ? item : undefined" :notification="type === 'notification' ? item : undefined" @update="$emit('update', $event)"
<component :is="itemComponent" :note="type === 'status' ? item : undefined" :notification="type === 'notification' ? item : (undefined as any)" @update="$emit('update', $event)"
@delete="$emit('delete', item?.id)" />
</template>
<script lang="ts" setup>
import type { Notification, Status } from "@versia/client/types";
import type { Notification, Status } from "@versia/client/schemas";
import type { z } from "zod";
import Thread from "../notes/thread.vue";
import NotificationItem from "../notifications/notification.vue";
const props = defineProps<{
item?: Status | Notification;
item?: z.infer<typeof Status> | z.infer<typeof Notification>;
type: "status" | "notification";
}>();
@ -17,9 +18,11 @@ const itemComponent = computed(() => {
if (props.type === "status") {
return Thread;
}
if (props.type === "notification") {
return NotificationItem;
}
return null;
});
</script>

View file

@ -40,8 +40,9 @@
</template>
<script lang="ts" setup>
import type { Notification, Status } from "@versia/client/types";
import type { Notification, Status } from "@versia/client/schemas";
import { useIntersectionObserver } from "@vueuse/core";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import NoPosts from "../errors/NoPosts.vue";
import ReachedEnd from "../errors/ReachedEnd.vue";
@ -50,7 +51,7 @@ import { Button } from "../ui/button";
import TimelineItem from "./timeline-item.vue";
const props = defineProps<{
items: Status[] | Notification[];
items: z.infer<typeof Status>[] | z.infer<typeof Notification>[];
type: "status" | "notification";
isLoading: boolean;
hasReachedEnd: boolean;
@ -58,14 +59,15 @@ const props = defineProps<{
loadNext: () => void;
loadPrev: () => void;
removeItem: (id: string) => void;
updateItem: ((item: Status) => void) | ((item: Notification) => void);
updateItem:
| ((item: z.infer<typeof Status>) => void)
| ((item: z.infer<typeof Notification>) => void);
}>();
const emit = defineEmits<(e: "update") => void>();
const loadMoreTrigger = ref<HTMLElement | null>(null);
// @ts-expect-error Too complex?
useIntersectionObserver(loadMoreTrigger, ([observer]) => {
if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) {
props.loadNext();

View file

@ -5,7 +5,7 @@ import { DrawerRoot } from "vaul-vue";
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
});
}) as DrawerRootProps;
const emits = defineEmits<DrawerRootEmits>();

View file

@ -1,15 +1,16 @@
import type { Client } from "@versia/client";
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
export const useAccount = (
client: MaybeRef<Client | null>,
accountId: MaybeRef<string | null>,
) => {
if (!client) {
return ref(null as Account | null);
return ref(null as z.infer<typeof Account> | null);
}
const output = ref(null as Account | null);
const output = ref(null as z.infer<typeof Account> | null);
watchEffect(() => {
if (toValue(accountId)) {

View file

@ -1,11 +1,15 @@
import type { Client } from "@versia/client";
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
export const useAccountFromAcct = (
client: MaybeRef<Client | null>,
acct: string,
): { account: Ref<Account | null>; isLoading: Ref<boolean> } => {
const output = ref(null as Account | null);
): {
account: Ref<z.infer<typeof Account> | null>;
isLoading: Ref<boolean>;
} => {
const output = ref(null as z.infer<typeof Account> | null);
const isLoading = ref(true);
ref(client)

View file

@ -1,11 +1,12 @@
import type { Client } from "@versia/client";
import type { Account } from "@versia/client/types";
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
export const useAccountSearch = (
client: MaybeRef<Client | null>,
q: string,
): Ref<Account[] | null> => {
const output = ref(null as Account[] | null);
): Ref<z.infer<typeof Account>[] | null> => {
const output = ref(null as z.infer<typeof Account>[] | null);
ref(client)
.value?.searchAccount(q, {

View file

@ -1,11 +1,12 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function useAccountTimeline(
client: Client,
accountId: string,
options: Partial<TimelineOptions<Status>> = {},
options: Partial<TimelineOptions<z.infer<typeof Status>>> = {},
) {
return useTimeline(client, {
fetchFunction: (client, opts) =>

View file

@ -1,8 +1,13 @@
import type { ApplicationData } from "@versia/client/types";
import type { CredentialApplication } from "@versia/client/schemas";
import { StorageSerializers } from "@vueuse/core";
import type { z } from "zod";
export const useAppData = () => {
return useLocalStorage<ApplicationData | null>("versia:app_data", null, {
serializer: StorageSerializers.object,
});
return useLocalStorage<z.infer<typeof CredentialApplication> | null>(
"versia:app_data",
null,
{
serializer: StorageSerializers.object,
},
);
};

View file

@ -1,5 +1,5 @@
import type { Client } from "@versia/client";
import type { RolePermission } from "@versia/client/types";
import type { RolePermission } from "@versia/client/schemas";
import { toast } from "vue-sonner";
import * as m from "~/paraglide/messages.js";

View file

@ -1,9 +1,11 @@
import { Client, type Token } from "@versia/client";
import { Client } from "@versia/client";
import type { Token } from "@versia/client/schemas";
import { toast } from "vue-sonner";
import type { z } from "zod";
export const useClient = (
origin?: MaybeRef<URL>,
customToken: MaybeRef<Token | null> = null,
customToken: MaybeRef<z.infer<typeof Token> | null> = null,
): Ref<Client> => {
const apiHost = window.location.origin;
const domain = identity.value?.instance.domain;
@ -15,10 +17,14 @@ export const useClient = (
toValue(customToken)?.access_token ??
identity.value?.tokens.access_token ??
undefined,
(error) => {
toast.error(
error.response.data.error ?? "No error message provided",
);
{
globalCatch: (error) => {
toast.error(
error.response.data.error ??
"No error message provided",
);
},
throwOnError: false,
},
),
) as Ref<Client>;

View file

@ -1,27 +1,28 @@
import type { Account, Attachment, Status } from "@versia/client/types";
import type { Account, Attachment, Status } from "@versia/client/schemas";
import mitt from "mitt";
import type { z } from "zod";
import type { Identity } from "./Identities";
type ApplicationEvents = {
"note:reply": Status;
"note:delete": Status;
"note:edit": Status;
"note:like": Status;
"note:unlike": Status;
"note:reblog": Status;
"note:unreblog": Status;
"note:quote": Status;
"note:report": Status;
"note:reply": z.infer<typeof Status>;
"note:delete": z.infer<typeof Status>;
"note:edit": z.infer<typeof Status>;
"note:like": z.infer<typeof Status>;
"note:unlike": z.infer<typeof Status>;
"note:reblog": z.infer<typeof Status>;
"note:unreblog": z.infer<typeof Status>;
"note:quote": z.infer<typeof Status>;
"note:report": z.infer<typeof Status>;
"composer:open": undefined;
"composer:reply": Status;
"composer:quote": Status;
"composer:edit": Status;
"composer:send": Status;
"composer:send-edit": Status;
"composer:reply": z.infer<typeof Status>;
"composer:quote": z.infer<typeof Status>;
"composer:edit": z.infer<typeof Status>;
"composer:send": z.infer<typeof Status>;
"composer:send-edit": z.infer<typeof Status>;
"composer:close": undefined;
"account:report": Account;
"account:update": Account;
"attachment:view": Attachment;
"account:report": z.infer<typeof Account>;
"account:update": z.infer<typeof Account>;
"attachment:view": z.infer<typeof Attachment>;
"identity:change": Identity;
"preferences:open": undefined;
error: {

View file

@ -1,16 +1,13 @@
import type { Client } from "@versia/client";
type ExtendedDescription = {
updated_at: string;
content: string;
};
import type { ExtendedDescription } from "@versia/client/schemas";
import type { z } from "zod";
export const useExtendedDescription = (client: MaybeRef<Client | null>) => {
if (!ref(client).value) {
return ref(null as ExtendedDescription | null);
return ref(null as z.infer<typeof ExtendedDescription> | null);
}
const output = ref(null as ExtendedDescription | null);
const output = ref(null as z.infer<typeof ExtendedDescription> | null);
ref(client)
.value?.getInstanceExtendedDescription()

View file

@ -1,10 +1,11 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function useGlobalTimeline(
client: Client,
options: Partial<TimelineOptions<Status>> = {},
options: Partial<TimelineOptions<z.infer<typeof Status>>> = {},
) {
return useTimeline(client, {
// TODO: Implement global timeline in client sdk

View file

@ -1,10 +1,11 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function useHomeTimeline(
client: Client,
options: Partial<TimelineOptions<Status>> = {},
options: Partial<TimelineOptions<z.infer<typeof Status>>> = {},
) {
return useTimeline(client, {
fetchFunction: (client, opts) => client.getHomeTimeline(opts),

View file

@ -1,23 +1,24 @@
import type { Token } from "@versia/client";
import type {
Account,
Emoji,
CustomEmoji,
Instance,
RolePermission,
} from "@versia/client/types";
Token,
} from "@versia/client/schemas";
import { StorageSerializers, useLocalStorage } from "@vueuse/core";
import { ref, watch } from "vue";
import type { z } from "zod";
/**
* Represents an identity with associated tokens, account, instance, permissions, and emojis.
*/
export interface Identity {
id: string;
tokens: Token;
account: Account;
instance: Instance;
tokens: z.infer<typeof Token>;
account: z.infer<typeof Account>;
instance: z.infer<typeof Instance>;
permissions: RolePermission[];
emojis: Emoji[];
emojis: z.infer<typeof CustomEmoji>[];
}
/**

View file

@ -1,5 +1,6 @@
import type { Client } from "@versia/client";
import type { ExtendedDescription, Instance } from "@versia/client/types";
import type { Instance, TermsOfService } from "@versia/client/schemas";
import type { z } from "zod";
export const useInstance = () => {
return computed(() => identity.value?.instance);
@ -7,10 +8,10 @@ export const useInstance = () => {
export const useInstanceFromClient = (client: MaybeRef<Client>) => {
if (!client) {
return ref(null as Instance | null);
return ref(null as z.infer<typeof Instance> | null);
}
const output = ref(null as Instance | null);
const output = ref(null as z.infer<typeof Instance> | null);
watchEffect(() => {
toValue(client)
@ -25,10 +26,10 @@ export const useInstanceFromClient = (client: MaybeRef<Client>) => {
export const useTos = (client: MaybeRef<Client>) => {
if (!client) {
return ref(null as ExtendedDescription | null);
return ref(null as z.infer<typeof TermsOfService> | null);
}
const output = ref(null as ExtendedDescription | null);
const output = ref(null as z.infer<typeof TermsOfService> | null);
watchEffect(() => {
toValue(client)

View file

@ -1,10 +1,11 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function useLocalTimeline(
client: Client,
options: Partial<TimelineOptions<Status>> = {},
options: Partial<TimelineOptions<z.infer<typeof Status>>> = {},
) {
return useTimeline(client, {
fetchFunction: (client, opts) => client.getLocalTimeline(opts),

View file

@ -1,15 +1,16 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
export const useNote = (
client: MaybeRef<Client | null>,
noteId: MaybeRef<string | null>,
) => {
if (!(toValue(client) && toValue(noteId))) {
return ref(null as Status | null);
return ref(null as z.infer<typeof Status> | null);
}
const output = ref(null as Status | null);
const output = ref(null as z.infer<typeof Status> | null);
watchEffect(() => {
toValue(noteId) &&

View file

@ -1,15 +1,16 @@
import type { Client } from "@versia/client";
import type { Context } from "@versia/client/types";
import type { Context } from "@versia/client/schemas";
import type { z } from "zod";
export const useNoteContext = (
client: MaybeRef<Client | null>,
noteId: MaybeRef<string | null>,
) => {
if (!ref(client).value) {
return ref(null as Context | null);
return ref(null as z.infer<typeof Context> | null);
}
const output = ref(null as Context | null);
const output = ref(null as z.infer<typeof Context> | null);
watchEffect(() => {
if (toValue(noteId)) {

View file

@ -1,10 +1,11 @@
import type { Client } from "@versia/client";
import type { Notification } from "@versia/client/types";
import type { Notification } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function useNotificationTimeline(
client: Client,
options: Partial<TimelineOptions<Notification>> = {},
options: Partial<TimelineOptions<z.infer<typeof Notification>>> = {},
) {
return useTimeline(client, {
fetchFunction: (client, opts) => client.getNotifications(opts),

View file

@ -1,10 +1,11 @@
import type { Client } from "@versia/client";
import type { Status } from "@versia/client/types";
import type { Status } from "@versia/client/schemas";
import type { z } from "zod";
import { type TimelineOptions, useTimeline } from "./Timeline";
export function usePublicTimeline(
client: Client,
options: Partial<TimelineOptions<Status>> = {},
options: Partial<TimelineOptions<z.infer<typeof Status>>> = {},
) {
return useTimeline(client, {
fetchFunction: (client, opts) => client.getPublicTimeline(opts),

View file

@ -1,11 +1,12 @@
import type { Client } from "@versia/client";
import type { Relationship } from "@versia/client/types";
import type { Relationship } from "@versia/client/schemas";
import type { z } from "zod";
export const useRelationship = (
client: MaybeRef<Client | null>,
accountId: MaybeRef<string | null>,
) => {
const relationship = ref(null as Relationship | null);
const relationship = ref(null as z.infer<typeof Relationship> | null);
const isLoading = ref(false);
if (!identity.value) {

View file

@ -1,15 +1,16 @@
import type { Client } from "@versia/client";
import type { Account, Mention } from "@versia/client/types";
import type { Account, Mention } from "@versia/client/schemas";
import type { z } from "zod";
export const useResolveMentions = (
mentions: Ref<Mention[]>,
mentions: Ref<z.infer<typeof Mention>[]>,
client: Client | null,
): Ref<Account[]> => {
): Ref<z.infer<typeof Account>[]> => {
if (!client) {
return ref([]);
}
const output = ref<Account[]>([]);
const output = ref<z.infer<typeof Account>[]>([]);
watch(
mentions,

View file

@ -1,6 +1,9 @@
import type { Instance } from "@versia/client/types";
import type { Instance } from "@versia/client/schemas";
import type { z } from "zod";
export const useSSOConfig = (): Ref<Instance["sso"] | null> => {
export const useSSOConfig = (): Ref<z.infer<
typeof Instance.shape.sso
> | null> => {
const instance = useInstance();
return computed(() => instance.value?.sso || null);

View file

@ -1,6 +1,7 @@
import type { Client, Output } from "@versia/client";
import type { Notification, Status } from "@versia/client/types";
import type { Notification, Status } from "@versia/client/schemas";
import { useIntervalFn } from "@vueuse/core";
import type { z } from "zod";
export interface TimelineOptions<T> {
fetchFunction: (client: Client, options: object) => Promise<Output<T[]>>;
@ -8,10 +9,9 @@ export interface TimelineOptions<T> {
limit?: number;
}
export function useTimeline<T extends Status | Notification>(
client: Client,
options: TimelineOptions<T>,
) {
export function useTimeline<
T extends z.infer<typeof Status> | z.infer<typeof Notification>,
>(client: Client, options: TimelineOptions<T>) {
const items = ref<T[]>([]) as Ref<T[]>;
const isLoading = ref(false);
const hasReachedEnd = ref(false);

View file

@ -248,6 +248,7 @@ export default defineNuxtConfig({
htmlAttrs: { lang: "en-us" },
},
rootAttrs: {
// @ts-expect-error Nonstandard HTML attribute for Vaul
"vaul-drawer-wrapper": true,
},
keepalive: true,

View file

@ -1,112 +1,112 @@
{
"name": "@versia/frontend",
"version": "0.8.0-alpha",
"private": true,
"description": "Beautiful, powerful and responsive web client for Versia Server.",
"type": "module",
"license": "AGPL-3.0",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/versia-pub/frontend.git"
},
"scripts": {
"build": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt build",
"dev": "bun nuxt dev --host versia-fe.localhost",
"generate": "nuxt generate",
"emojis:generate": "bun run utils/emojis.ts",
"rebuild-i18n": "paraglide-js compile --project ./project.inlang --outdir ./paraglide",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt prepare",
"lint": "bunx @biomejs/biome check .",
"check": "bunx tsc -p ."
},
"dependencies": {
"@nuxt/fonts": "^0.11.4",
"@nuxtjs/color-mode": "3.5.2",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/vue-table": "^8.21.3",
"@tiptap/extension-highlight": "^2.12.0",
"@tiptap/extension-image": "^2.12.0",
"@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-mention": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/extension-subscript": "^2.12.0",
"@tiptap/extension-superscript": "^2.12.0",
"@tiptap/extension-task-item": "^2.12.0",
"@tiptap/extension-task-list": "^2.12.0",
"@tiptap/extension-underline": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@tiptap/suggestion": "^2.12.0",
"@tiptap/vue-3": "^2.12.0",
"@vee-validate/zod": "^4.15.0",
"@versia/client": "0.1.5",
"@videojs-player/vue": "^1.0.0",
"@vite-pwa/nuxt": "^1.0.1",
"@vueuse/core": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.6.0",
"fuzzysort": "^3.1.0",
"html-to-text": "^9.0.5",
"lucide-vue-next": "^0.511.0",
"magic-regexp": "^0.10.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"nuxt": "^3.17.4",
"nuxt-security": "^2.2.0",
"reka-ui": "^2.2.1",
"shadcn-nuxt": "2.1.0",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.0",
"vaul-vue": "^0.4.1",
"vee-validate": "^4.15.0",
"virtua": "^0.41.2",
"vue": "^3.5.14",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1",
"vue-sonner": "^2.0.0",
"zod": "^3.25.23"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@iconify-json/fluent-emoji": "^1.2.3",
"@iconify-json/fluent-emoji-flat": "^1.2.3",
"@iconify-json/noto": "^1.2.3",
"@iconify-json/twemoji": "^1.2.2",
"@iconify/utils": "^2.3.0",
"@inlang/paraglide-js": "2.0.13",
"@inlang/plugin-m-function-matcher": "^2.0.10",
"@inlang/plugin-message-format": "^4.0.0",
"@tailwindcss/forms": "^0.5.10",
"@types/html-to-text": "^9.0.4",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"@parcel/watcher",
"@tailwindcss/oxide",
"esbuild",
"json-editor-vue",
"vue-demi"
]
"name": "@versia/frontend",
"version": "0.8.0-alpha",
"private": true,
"description": "Beautiful, powerful and responsive web client for Versia Server.",
"type": "module",
"license": "AGPL-3.0",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/versia-pub/frontend.git"
},
"scripts": {
"build": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt build",
"dev": "bun nuxt dev --host versia-fe.localhost",
"generate": "nuxt generate",
"emojis:generate": "bun run utils/emojis.ts",
"rebuild-i18n": "paraglide-js compile --project ./project.inlang --outdir ./paraglide",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt prepare",
"lint": "bunx @biomejs/biome check .",
"check": "bunx tsc -p ."
},
"dependencies": {
"@nuxt/fonts": "^0.11.4",
"@nuxtjs/color-mode": "3.5.2",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/vue-table": "^8.21.3",
"@tiptap/extension-highlight": "^2.12.0",
"@tiptap/extension-image": "^2.12.0",
"@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-mention": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/extension-subscript": "^2.12.0",
"@tiptap/extension-superscript": "^2.12.0",
"@tiptap/extension-task-item": "^2.12.0",
"@tiptap/extension-task-list": "^2.12.0",
"@tiptap/extension-underline": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@tiptap/suggestion": "^2.12.0",
"@tiptap/vue-3": "^2.12.0",
"@vee-validate/zod": "^4.15.0",
"@versia/client": "0.2.0-alpha.2",
"@videojs-player/vue": "^1.0.0",
"@vite-pwa/nuxt": "^1.0.1",
"@vueuse/core": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.6.0",
"fuzzysort": "^3.1.0",
"html-to-text": "^9.0.5",
"lucide-vue-next": "^0.511.0",
"magic-regexp": "^0.10.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"nuxt": "^3.17.4",
"nuxt-security": "^2.2.0",
"reka-ui": "^2.2.1",
"shadcn-nuxt": "2.1.0",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.0",
"vaul-vue": "^0.4.1",
"vee-validate": "^4.15.0",
"virtua": "^0.41.2",
"vue": "^3.5.14",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1",
"vue-sonner": "^2.0.0",
"zod": "^3.25.23"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@iconify-json/fluent-emoji": "^1.2.3",
"@iconify-json/fluent-emoji-flat": "^1.2.3",
"@iconify-json/noto": "^1.2.3",
"@iconify-json/twemoji": "^1.2.2",
"@iconify/utils": "^2.3.0",
"@inlang/paraglide-js": "2.0.13",
"@inlang/plugin-m-function-matcher": "^2.0.10",
"@inlang/plugin-message-format": "^4.0.0",
"@tailwindcss/forms": "^0.5.10",
"@types/html-to-text": "^9.0.4",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"@parcel/watcher",
"@tailwindcss/oxide",
"esbuild",
"json-editor-vue",
"vue-demi"
]
}

View file

@ -1,53 +1,60 @@
import type { Emoji } from "@versia/client/types";
import type { CustomEmoji } from "@versia/client/schemas";
import type { z } from "zod";
const emojisRegex =
/\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu;
const incorrectEmojisRegex = /^[#*0-9©®]$/;
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive<HTMLElement, Emoji[]>("render-emojis", {
beforeMount(el, binding) {
// Replace emoji shortcodes with images
if (preferences.custom_emojis.value) {
el.innerHTML = el.innerHTML.replace(
/:([a-zA-Z0-9_-]+):/g,
(match, emoji) => {
const emojiData = binding.value.find(
(e) => e.shortcode === emoji,
);
nuxtApp.vueApp.directive<HTMLElement, z.infer<typeof CustomEmoji>[]>(
"render-emojis",
{
beforeMount(el, binding) {
// Replace emoji shortcodes with images
if (preferences.custom_emojis.value) {
el.innerHTML = el.innerHTML.replace(
/:([a-zA-Z0-9_-]+):/g,
(match, emoji) => {
const emojiData = binding.value.find(
(e) => e.shortcode === emoji,
);
if (!emojiData) {
return match;
}
if (!emojiData) {
return match;
}
const image = document.createElement("img");
image.src = emojiData.url;
image.alt = `:${emoji}:`;
image.title = emojiData.shortcode;
image.className =
"h-[1lh] align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out";
return image.outerHTML;
},
);
}
if (preferences.emoji_theme.value !== "native") {
el.innerHTML = el.innerHTML.replace(emojisRegex, (match) => {
if (incorrectEmojisRegex.test(match)) {
return match;
}
return `<img src="/emojis/${preferences.emoji_theme.value}/${match}.svg" alt="${match}" class="h-[1em] inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out">`;
});
}
// Make all links that don't open to the same origin open in a new tab
for (const link of el.querySelectorAll("a")) {
if (link.hostname !== location.hostname) {
link.target = "_blank";
link.rel = "noopener noreferrer";
const image = document.createElement("img");
image.src = emojiData.url;
image.alt = `:${emoji}:`;
image.title = emojiData.shortcode;
image.className =
"h-[1lh] align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out";
return image.outerHTML;
},
);
}
}
if (preferences.emoji_theme.value !== "native") {
el.innerHTML = el.innerHTML.replace(
emojisRegex,
(match) => {
if (incorrectEmojisRegex.test(match)) {
return match;
}
return `<img src="/emojis/${preferences.emoji_theme.value}/${match}.svg" alt="${match}" class="h-[1em] inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out">`;
},
);
}
// Make all links that don't open to the same origin open in a new tab
for (const link of el.querySelectorAll("a")) {
if (link.hostname !== location.hostname) {
link.target = "_blank";
link.rel = "noopener noreferrer";
}
}
},
},
});
);
});

View file

@ -1,7 +1,7 @@
import type { Client } from "@versia/client";
import type { ApplicationData } from "@versia/client/types";
import type { CredentialApplication } from "@versia/client/schemas";
import { nanoid } from "nanoid";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { confirmModalService } from "~/components/modals/composable";
import pkg from "~/package.json";
import * as m from "~/paraglide/messages.js";
@ -23,7 +23,7 @@ export const askForInstance = async (): Promise<URL> => {
};
export const signIn = async (
appData: Ref<ApplicationData | null>,
appData: Ref<z.infer<typeof CredentialApplication> | null>,
origin: URL,
) => {
const id = toast.loading(m.level_due_ox_greet());
@ -74,7 +74,7 @@ export const signIn = async (
export const signInWithCode = (
code: string,
appData: ApplicationData,
appData: z.infer<typeof CredentialApplication>,
origin: URL,
) => {
const client = useClient(origin);
@ -126,7 +126,7 @@ export const signInWithCode = (
};
export const signOut = async (
appData: ApplicationData | null,
appData: z.infer<typeof CredentialApplication> | null,
identityToRevoke: Identity,
) => {
const id = toast.loading("Signing out...");