mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
Some checks failed
This commit is contained in:
parent
8debe97f63
commit
7f7cf20311
386 changed files with 2376 additions and 2332 deletions
26
app/composables/Account.ts
Normal file
26
app/composables/Account.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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 z.infer<typeof Account> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof Account> | null);
|
||||
|
||||
watchEffect(() => {
|
||||
if (toValue(accountId)) {
|
||||
toValue(client)
|
||||
?.getAccount(toValue(accountId) ?? "")
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
23
app/composables/AccountAcct.ts
Normal file
23
app/composables/AccountAcct.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Client } from "@versia/client";
|
||||
import type { Account } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useAccountFromAcct = (
|
||||
client: MaybeRef<Client | null>,
|
||||
acct: string,
|
||||
): {
|
||||
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)
|
||||
.value?.lookupAccount(acct)
|
||||
.then((res) => {
|
||||
isLoading.value = false;
|
||||
output.value = res.data;
|
||||
});
|
||||
|
||||
return { account: output, isLoading };
|
||||
};
|
||||
20
app/composables/AccountSearch.ts
Normal file
20
app/composables/AccountSearch.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { Client } from "@versia/client";
|
||||
import type { Account } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useAccountSearch = (
|
||||
client: MaybeRef<Client | null>,
|
||||
q: string,
|
||||
): Ref<z.infer<typeof Account>[] | null> => {
|
||||
const output = ref(null as z.infer<typeof Account>[] | null);
|
||||
|
||||
ref(client)
|
||||
.value?.searchAccount(q, {
|
||||
resolve: true,
|
||||
})
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
16
app/composables/AccountTimeline.ts
Normal file
16
app/composables/AccountTimeline.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Status>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
fetchFunction: (client, opts) =>
|
||||
client.getAccountStatuses(accountId, opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
13
app/composables/AppData.ts
Normal file
13
app/composables/AppData.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { CredentialApplication } from "@versia/client/schemas";
|
||||
import { StorageSerializers } from "@vueuse/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useAppData = () => {
|
||||
return useLocalStorage<z.infer<typeof CredentialApplication> | null>(
|
||||
"versia:app_data",
|
||||
null,
|
||||
{
|
||||
serializer: StorageSerializers.object,
|
||||
},
|
||||
);
|
||||
};
|
||||
64
app/composables/Audio.ts
Normal file
64
app/composables/Audio.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
export type AudioNames = "publish" | "like";
|
||||
|
||||
export type AudioManifest = Record<AudioNames, { src: string[] }>;
|
||||
|
||||
export const useAudio = (): {
|
||||
play: (name: AudioNames) => void;
|
||||
} => {
|
||||
const audio = new Audio();
|
||||
|
||||
const play = (name: AudioNames) => {
|
||||
const audioData = audioManifest.manifest.value?.[name];
|
||||
|
||||
if (!audioData) {
|
||||
throw new Error(`Audio not found: ${name}`);
|
||||
}
|
||||
|
||||
const src = audioData.src[
|
||||
Math.floor(Math.random() * audioData.src.length)
|
||||
] as string;
|
||||
|
||||
audio.src = src;
|
||||
audio.play();
|
||||
};
|
||||
|
||||
return { play };
|
||||
};
|
||||
|
||||
export const useAudioManifest = () => {
|
||||
const audioTheme = ref("misskey" as const);
|
||||
const url = computed(() => `/packs/audio/${audioTheme.value}.json`);
|
||||
|
||||
// Fetch from /packs/audio/:name.json
|
||||
const manifest = ref(null as null | AudioManifest);
|
||||
|
||||
// Fetch the manifest
|
||||
watch(
|
||||
url,
|
||||
async (url) => {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch audio theme manifest at ${url}`,
|
||||
);
|
||||
}
|
||||
|
||||
manifest.value = await response.json();
|
||||
|
||||
// Preload all audio files
|
||||
if (manifest.value) {
|
||||
for (const audioData of Object.values(manifest.value)) {
|
||||
for (const src of audioData.src) {
|
||||
new Audio(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return { audioTheme, manifest, url };
|
||||
};
|
||||
|
||||
export const audioManifest = useAudioManifest();
|
||||
67
app/composables/CacheRefresh.ts
Normal file
67
app/composables/CacheRefresh.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import type { Client } from "@versia/client";
|
||||
import type { RolePermission } from "@versia/client/schemas";
|
||||
import { toast } from "vue-sonner";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
export const useCacheRefresh = (client: MaybeRef<Client | null>) => {
|
||||
// Refresh custom emojis and instance data and me on every reload
|
||||
watch(
|
||||
[identity, client],
|
||||
async () => {
|
||||
console.info("Refreshing emoji, instance and account cache");
|
||||
if (identity.value) {
|
||||
toValue(client)
|
||||
?.verifyAccountCredentials()
|
||||
.then((res) => {
|
||||
if (identity.value) {
|
||||
identity.value.account = res.data;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const code = err.response.status;
|
||||
|
||||
if (code === 401) {
|
||||
// Reset tokenData
|
||||
identity.value = null;
|
||||
toast.error(m.fancy_this_wasp_renew(), {
|
||||
description: m.real_weird_deer_stop(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
toValue(client)
|
||||
?.getInstanceCustomEmojis()
|
||||
.then((res) => {
|
||||
if (identity.value) {
|
||||
identity.value.emojis = res.data;
|
||||
}
|
||||
});
|
||||
|
||||
toValue(client)
|
||||
?.getAccountRoles(identity.value.account.id)
|
||||
.then((res) => {
|
||||
const roles = res.data;
|
||||
|
||||
// Get all permissions and deduplicate
|
||||
const permissions = roles
|
||||
?.flatMap((r) => r.permissions)
|
||||
.filter((p, i, arr) => arr.indexOf(p) === i);
|
||||
|
||||
if (identity.value) {
|
||||
identity.value.permissions =
|
||||
permissions as unknown as RolePermission[];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toValue(client)
|
||||
?.getInstance()
|
||||
.then((res) => {
|
||||
if (identity.value) {
|
||||
identity.value.instance = res.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
{ flush: "sync", immediate: true },
|
||||
);
|
||||
};
|
||||
33
app/composables/Client.ts
Normal file
33
app/composables/Client.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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<z.infer<typeof Token> | null> = null,
|
||||
): Ref<Client> => {
|
||||
const apiHost = window.location.origin;
|
||||
const domain = identity.value?.instance.domain;
|
||||
|
||||
return ref(
|
||||
new Client(
|
||||
toValue(origin) ??
|
||||
(domain ? new URL(`https://${domain}`) : new URL(apiHost)),
|
||||
toValue(customToken)?.access_token ??
|
||||
identity.value?.tokens.access_token ??
|
||||
undefined,
|
||||
{
|
||||
globalCatch: (error) => {
|
||||
toast.error(
|
||||
error.response.data.error ??
|
||||
"No error message provided",
|
||||
);
|
||||
},
|
||||
throwOnError: false,
|
||||
},
|
||||
),
|
||||
) as Ref<Client>;
|
||||
};
|
||||
|
||||
export const client = useClient();
|
||||
85
app/composables/Config.ts
Normal file
85
app/composables/Config.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
export const useConfig = () => {
|
||||
return {
|
||||
NOTES_PER_PAGE: 20,
|
||||
RECOMMENDED_CLIENTS: [
|
||||
{
|
||||
name: "Megalodon",
|
||||
icon: "https://sk22.github.io/megalodon/mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png",
|
||||
link: "https://sk22.github.io/megalodon/",
|
||||
},
|
||||
],
|
||||
COMPOSER_SPLASHES: [
|
||||
"What's on your mind?",
|
||||
"What's happening?",
|
||||
"Meow meow meow meow meow",
|
||||
"Just arrived at Luna Nova Academy",
|
||||
"I'm a little teapot, short and stout",
|
||||
"Hey, you. You're finally awake. You were trying to cross the border, right?",
|
||||
"Aren't you a little short for a stormtrooper?",
|
||||
"I'm a leaf on the wind. Watch how I soar.",
|
||||
"I am Groot.",
|
||||
"Hello everybody, my name is Markiplier",
|
||||
"I'm Commander Shepard, and this is my favorite website on the Citadel",
|
||||
"I'm sorry, Dave. I'm afraid I can't do that.",
|
||||
"I am the Senate",
|
||||
"Check out my geek code!",
|
||||
"You've got mail!",
|
||||
"Dude, Where's My Potato?",
|
||||
"eau de potato",
|
||||
"The bee's knees!",
|
||||
"12 herbs and spices!",
|
||||
"Ceci n'est pas une post!",
|
||||
"sqrt(-1) love you!",
|
||||
"Cthulhu is mad... and is missing an eye!",
|
||||
"Versia: Build on blackbox technology!",
|
||||
"A goblin army is approaching from the west!",
|
||||
"git gud!",
|
||||
"Must have been the wind..",
|
||||
"I am the milkman, my milk is delicious.",
|
||||
"It's not out of the question, that you might have a very minor case, of serious brain damage",
|
||||
"I died",
|
||||
"Remember, switching to your secondary is faster than reloading!",
|
||||
"Everybody in america is a female.",
|
||||
"I am going to test in production",
|
||||
"War... war never changes.",
|
||||
"Fedi... fedi never changes.",
|
||||
"Finish him!",
|
||||
"The dopamine is a lie.",
|
||||
"I'll be back.",
|
||||
"My name is Guybrush Threepwood, and I want to be a pirate!",
|
||||
"It's dangerous to post alone! Take this.",
|
||||
"I used to be an poster like you, then I took an arrow to the knee.",
|
||||
"All your post are belong to us.",
|
||||
"I'm here to shitpost and chew bubblegum... and I'm all outta gum.",
|
||||
"Houston, we have a problem.",
|
||||
"Clever girl.",
|
||||
"Wibbly wobbly, timey wimey... stuff.",
|
||||
"Bow ties are cool.",
|
||||
"I'm the Doctor, and you're probably not.",
|
||||
"I'm a madman with a box.",
|
||||
"Geronimo!",
|
||||
"It's bigger on the inside!",
|
||||
"I'm going to make him an offer he can't refuse.",
|
||||
"Hello there.",
|
||||
"I'll post what she's posting.",
|
||||
"To infinity... and beyond!",
|
||||
"E.T. phone home.",
|
||||
"Just keep posting!",
|
||||
"Just one more post bro",
|
||||
"I am the one who knocks.",
|
||||
"You've lost the game!",
|
||||
],
|
||||
DEVELOPER_HANDLES: [
|
||||
"jessew@social.lysand.org",
|
||||
"jessew@beta.versia.social",
|
||||
"jessew@versia.social",
|
||||
"jessew@vs.cpluspatch.com",
|
||||
"aprl@social.lysand.org",
|
||||
"aprl@beta.versia.social",
|
||||
"aprl@versia.social",
|
||||
"graphite@social.lysand.org",
|
||||
"jessew@mk.cpluspatch.com",
|
||||
"graphite@shonk.phite.ro",
|
||||
],
|
||||
};
|
||||
};
|
||||
38
app/composables/EventBus.ts
Normal file
38
app/composables/EventBus.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
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": 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": 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": z.infer<typeof Account>;
|
||||
"account:update": z.infer<typeof Account>;
|
||||
"attachment:view": z.infer<typeof Attachment>;
|
||||
"identity:change": Identity;
|
||||
"preferences:open": undefined;
|
||||
error: {
|
||||
code: string;
|
||||
title: string;
|
||||
message: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const emitter = mitt<ApplicationEvents>();
|
||||
|
||||
export const useEvent = emitter.emit;
|
||||
export const useListen = emitter.on;
|
||||
19
app/composables/ExtendedDescription.ts
Normal file
19
app/composables/ExtendedDescription.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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 z.infer<typeof ExtendedDescription> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof ExtendedDescription> | null);
|
||||
|
||||
ref(client)
|
||||
.value?.getInstanceExtendedDescription()
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
15
app/composables/GlobalTimeline.ts
Normal file
15
app/composables/GlobalTimeline.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Status>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
// TODO: Implement global timeline in client sdk
|
||||
fetchFunction: (client, opts) => client.getPublicTimeline(opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
14
app/composables/HomeTimeline.ts
Normal file
14
app/composables/HomeTimeline.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Status>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
fetchFunction: (client, opts) => client.getHomeTimeline(opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
104
app/composables/Identities.ts
Normal file
104
app/composables/Identities.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import type {
|
||||
Account,
|
||||
CustomEmoji,
|
||||
Instance,
|
||||
RolePermission,
|
||||
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: z.infer<typeof Token>;
|
||||
account: z.infer<typeof Account>;
|
||||
instance: z.infer<typeof Instance>;
|
||||
permissions: RolePermission[];
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to manage multiple identities.
|
||||
* @returns A reactive reference to an array of identities.
|
||||
*/
|
||||
function useIdentities(): Ref<Identity[]> {
|
||||
return useLocalStorage<Identity[]>("versia:identities", [], {
|
||||
serializer: StorageSerializers.object,
|
||||
});
|
||||
}
|
||||
|
||||
export const identities = useIdentities();
|
||||
|
||||
const currentId = useLocalStorage<string | null>(
|
||||
"versia:identities:current",
|
||||
null,
|
||||
);
|
||||
|
||||
const current = ref<Identity | null>(null);
|
||||
|
||||
/**
|
||||
* Composable to manage the current identity.
|
||||
* @returns A reactive reference to the current identity or null if not set.
|
||||
*/
|
||||
function useCurrentIdentity(): Ref<Identity | null> {
|
||||
// Initialize current identity
|
||||
function updateCurrentIdentity() {
|
||||
current.value =
|
||||
identities.value.find((i) => i.id === currentId.value) ?? null;
|
||||
}
|
||||
|
||||
// Watch for changes in identities
|
||||
watch(
|
||||
identities,
|
||||
(ids) => {
|
||||
if (ids.length === 0) {
|
||||
current.value = null;
|
||||
currentId.value = null;
|
||||
} else {
|
||||
updateCurrentIdentity();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for changes in currentId
|
||||
watch(currentId, updateCurrentIdentity);
|
||||
|
||||
// Watch for changes in current identity
|
||||
watch(
|
||||
current,
|
||||
(newCurrent) => {
|
||||
if (newCurrent) {
|
||||
currentId.value = newCurrent.id;
|
||||
const index = identities.value.findIndex(
|
||||
(i) => i.id === newCurrent.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
// Update existing identity
|
||||
identities.value[index] = newCurrent;
|
||||
} else {
|
||||
// Add new identity
|
||||
identities.value.push(newCurrent);
|
||||
}
|
||||
} else {
|
||||
// Remove current identity
|
||||
identities.value = identities.value.filter(
|
||||
(i) => i.id !== currentId.value,
|
||||
);
|
||||
currentId.value = identities.value[0]?.id ?? null;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Initial setup
|
||||
updateCurrentIdentity();
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
export const identity = useCurrentIdentity();
|
||||
43
app/composables/Instance.ts
Normal file
43
app/composables/Instance.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { Client } from "@versia/client";
|
||||
import type { Instance, TermsOfService } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useInstance = () => {
|
||||
return computed(() => identity.value?.instance);
|
||||
};
|
||||
|
||||
export const useInstanceFromClient = (client: MaybeRef<Client>) => {
|
||||
if (!client) {
|
||||
return ref(null as z.infer<typeof Instance> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof Instance> | null);
|
||||
|
||||
watchEffect(() => {
|
||||
toValue(client)
|
||||
?.getInstance()
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export const useTos = (client: MaybeRef<Client>) => {
|
||||
if (!client) {
|
||||
return ref(null as z.infer<typeof TermsOfService> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof TermsOfService> | null);
|
||||
|
||||
watchEffect(() => {
|
||||
toValue(client)
|
||||
?.getInstanceTermsOfService()
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
3
app/composables/Language.ts
Normal file
3
app/composables/Language.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const useLanguage = () => {
|
||||
return computed(() => preferences.language.value);
|
||||
};
|
||||
25
app/composables/LinkedSSO.ts
Normal file
25
app/composables/LinkedSSO.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Client } from "@versia/client";
|
||||
|
||||
type SSOProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export const useLinkedSSO = (client: MaybeRef<Client>) => {
|
||||
if (!client) {
|
||||
return ref([] as SSOProvider[]);
|
||||
}
|
||||
|
||||
const output = ref([] as SSOProvider[]);
|
||||
|
||||
watchEffect(() => {
|
||||
toValue(client)
|
||||
?.get<SSOProvider[]>("/api/v1/sso")
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
14
app/composables/LocalTimeline.ts
Normal file
14
app/composables/LocalTimeline.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Status>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
fetchFunction: (client, opts) => client.getLocalTimeline(opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
25
app/composables/Note.ts
Normal file
25
app/composables/Note.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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 z.infer<typeof Status> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof Status> | null);
|
||||
|
||||
watchEffect(() => {
|
||||
toValue(noteId) &&
|
||||
toValue(client)
|
||||
?.getStatus(toValue(noteId) as string)
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
26
app/composables/NoteContext.ts
Normal file
26
app/composables/NoteContext.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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 z.infer<typeof Context> | null);
|
||||
}
|
||||
|
||||
const output = ref(null as z.infer<typeof Context> | null);
|
||||
|
||||
watchEffect(() => {
|
||||
if (toValue(noteId)) {
|
||||
ref(client)
|
||||
.value?.getStatusContext(toValue(noteId) ?? "")
|
||||
.then((res) => {
|
||||
output.value = res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
14
app/composables/NotificationTimeline.ts
Normal file
14
app/composables/NotificationTimeline.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Notification>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
fetchFunction: (client, opts) => client.getNotifications(opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
3
app/composables/Permissions.ts
Normal file
3
app/composables/Permissions.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const usePermissions = () => {
|
||||
return computed(() => identity.value?.permissions ?? []);
|
||||
};
|
||||
56
app/composables/Preference.ts
Normal file
56
app/composables/Preference.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { StorageSerializers } from "@vueuse/core";
|
||||
import { preferences as prefs } from "~/components/preferences/preferences";
|
||||
|
||||
type SerializedPreferences = {
|
||||
[K in keyof typeof prefs]: (typeof prefs)[K]["options"]["defaultValue"];
|
||||
};
|
||||
|
||||
const usePreferences = (): {
|
||||
[K in keyof typeof prefs]: WritableComputedRef<
|
||||
(typeof prefs)[K]["options"]["defaultValue"]
|
||||
>;
|
||||
} => {
|
||||
const localStorage = useLocalStorage<SerializedPreferences>(
|
||||
"versia:preferences",
|
||||
Object.fromEntries(
|
||||
Object.entries(prefs).map(([key, value]) => [
|
||||
key,
|
||||
value.options.defaultValue,
|
||||
]),
|
||||
) as SerializedPreferences,
|
||||
{
|
||||
serializer: {
|
||||
read(raw) {
|
||||
return StorageSerializers.object.read(raw);
|
||||
},
|
||||
write(value) {
|
||||
return StorageSerializers.object.write(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(prefs).map(([key, value]) => [
|
||||
key,
|
||||
computed({
|
||||
get() {
|
||||
return (
|
||||
localStorage.value[key as keyof typeof prefs] ??
|
||||
value.options.defaultValue
|
||||
);
|
||||
},
|
||||
set(newValue) {
|
||||
// @ts-expect-error Key is marked as readonly in the type
|
||||
localStorage.value[key] = newValue;
|
||||
},
|
||||
}),
|
||||
]),
|
||||
) as {
|
||||
[K in keyof typeof prefs]: WritableComputedRef<
|
||||
(typeof prefs)[K]["options"]["defaultValue"]
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export const preferences = usePreferences();
|
||||
14
app/composables/PublicTimeline.ts
Normal file
14
app/composables/PublicTimeline.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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<z.infer<typeof Status>>> = {},
|
||||
) {
|
||||
return useTimeline(client, {
|
||||
fetchFunction: (client, opts) => client.getPublicTimeline(opts),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
49
app/composables/Relationship.ts
Normal file
49
app/composables/Relationship.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Client } from "@versia/client";
|
||||
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 z.infer<typeof Relationship> | null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
if (!identity.value) {
|
||||
return { relationship, isLoading };
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (toValue(accountId)) {
|
||||
toValue(client)
|
||||
?.getRelationship(toValue(accountId) ?? "")
|
||||
.then((res) => {
|
||||
relationship.value = res.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(relationship, (newOutput, oldOutput) => {
|
||||
if (newOutput !== oldOutput && newOutput && oldOutput) {
|
||||
if (newOutput?.following !== oldOutput?.following) {
|
||||
isLoading.value = true;
|
||||
if (newOutput?.following) {
|
||||
toValue(client)
|
||||
?.followAccount(toValue(accountId) ?? "")
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
toValue(client)
|
||||
?.unfollowAccount(toValue(accountId) ?? "")
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
// FIXME: Add more relationship changes
|
||||
}
|
||||
});
|
||||
|
||||
return { relationship, isLoading };
|
||||
};
|
||||
29
app/composables/ResolveMentions.ts
Normal file
29
app/composables/ResolveMentions.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { Client } from "@versia/client";
|
||||
import type { Account, Mention } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useResolveMentions = (
|
||||
mentions: Ref<z.infer<typeof Mention>[]>,
|
||||
client: Client | null,
|
||||
): Ref<z.infer<typeof Account>[]> => {
|
||||
if (!client) {
|
||||
return ref([]);
|
||||
}
|
||||
|
||||
const output = ref<z.infer<typeof Account>[]>([]);
|
||||
|
||||
watch(
|
||||
mentions,
|
||||
async () => {
|
||||
output.value = await Promise.all(
|
||||
toValue(mentions).map(async (mention) => {
|
||||
const response = await client.getAccount(mention.id);
|
||||
return response.data;
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return output;
|
||||
};
|
||||
10
app/composables/SSOConfig.ts
Normal file
10
app/composables/SSOConfig.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Instance } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const useSSOConfig = (): Ref<z.infer<
|
||||
typeof Instance.shape.sso
|
||||
> | null> => {
|
||||
const instance = useInstance();
|
||||
|
||||
return computed(() => instance.value?.sso || null);
|
||||
};
|
||||
113
app/composables/Timeline.ts
Normal file
113
app/composables/Timeline.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import type { Client, Output } from "@versia/client";
|
||||
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[]>>;
|
||||
updateInterval?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
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);
|
||||
const error = ref<Error | null>(null);
|
||||
|
||||
const nextMaxId = ref<string | undefined>(undefined);
|
||||
const prevMinId = ref<string | undefined>(undefined);
|
||||
|
||||
const fetchItems = async (direction: "next" | "prev") => {
|
||||
if (isLoading.value || (direction === "next" && hasReachedEnd.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await options.fetchFunction(client, {
|
||||
limit: options.limit || 20,
|
||||
max_id: direction === "next" ? nextMaxId.value : undefined,
|
||||
min_id: direction === "prev" ? prevMinId.value : undefined,
|
||||
});
|
||||
|
||||
const newItems = response.data.filter(
|
||||
(item: T) =>
|
||||
!items.value.some((existing) => existing.id === item.id),
|
||||
);
|
||||
|
||||
if (direction === "next") {
|
||||
items.value.push(...newItems);
|
||||
if (newItems.length < (options.limit || 20)) {
|
||||
hasReachedEnd.value = true;
|
||||
}
|
||||
if (newItems.length > 0) {
|
||||
nextMaxId.value = newItems[newItems.length - 1]?.id;
|
||||
}
|
||||
} else {
|
||||
items.value.unshift(...newItems);
|
||||
if (newItems.length > 0) {
|
||||
prevMinId.value = newItems[0]?.id;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e instanceof Error ? e : new Error("An error occurred");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadNext = () => fetchItems("next");
|
||||
const loadPrev = () => fetchItems("prev");
|
||||
|
||||
const addItem = (newItem: T) => {
|
||||
items.value.unshift(newItem);
|
||||
};
|
||||
|
||||
const removeItem = (id: string) => {
|
||||
const index = items.value.findIndex((item) => item.id === id);
|
||||
if (index !== -1) {
|
||||
items.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const updateItem = (updatedItem: T) => {
|
||||
const index = items.value.findIndex(
|
||||
(item) => item.id === updatedItem.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
items.value[index] = updatedItem;
|
||||
}
|
||||
};
|
||||
|
||||
// Set up periodic updates
|
||||
const { pause, resume } = useIntervalFn(() => {
|
||||
loadPrev();
|
||||
}, options.updateInterval || 30000);
|
||||
|
||||
onMounted(() => {
|
||||
loadNext();
|
||||
resume();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
pause();
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
hasReachedEnd,
|
||||
error,
|
||||
loadNext,
|
||||
loadPrev,
|
||||
addItem,
|
||||
removeItem,
|
||||
updateItem,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue