mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
feat: ♻️ Change code to build in static mode, add timelines, profiles and notes, new design
This commit is contained in:
parent
9edfd5ac2d
commit
acd50ece9b
75 changed files with 1603 additions and 549 deletions
12
composables/Account.ts
Normal file
12
composables/Account.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
|
||||
export const useAccount = async (
|
||||
client: Mastodon | null,
|
||||
accountId: string,
|
||||
) => {
|
||||
if (process.server || !client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await client.getAccount(accountId)).data;
|
||||
};
|
||||
13
composables/AccountSearch.ts
Normal file
13
composables/AccountSearch.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
|
||||
export const useAccountSearch = async (client: Mastodon | null, q: string) => {
|
||||
if (process.server || !client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
await client.searchAccount(q, {
|
||||
resolve: true,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
3
composables/BaseUrl.ts
Normal file
3
composables/BaseUrl.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const useBaseUrl = () => {
|
||||
return ref(useRuntimeConfig().public.apiHost ?? useRequestURL().origin);
|
||||
};
|
||||
12
composables/Config.ts
Normal file
12
composables/Config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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/",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
9
composables/Instance.ts
Normal file
9
composables/Instance.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
|
||||
export const useInstance = async (client: Mastodon | null) => {
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await client.getInstance()).data;
|
||||
};
|
||||
13
composables/Megalodon.ts
Normal file
13
composables/Megalodon.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Mastodon } from "megalodon";
|
||||
|
||||
export const useMegalodon = async () => {
|
||||
/* if (process.server) {
|
||||
return null;
|
||||
} */
|
||||
|
||||
const baseUrl = useBaseUrl().value;
|
||||
|
||||
const client = new Mastodon(baseUrl);
|
||||
|
||||
return client;
|
||||
};
|
||||
9
composables/Note.ts
Normal file
9
composables/Note.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
|
||||
export const useNote = async (client: Mastodon | null, noteId: string) => {
|
||||
if (process.server || !client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await client.getStatus(noteId)).data;
|
||||
};
|
||||
12
composables/OAuthProviders.ts
Normal file
12
composables/OAuthProviders.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export const useOAuthProviders = async () => {
|
||||
const providers = await fetch(
|
||||
new URL("/oauth/providers", useBaseUrl().value),
|
||||
).then((d) => d.json());
|
||||
return ref(
|
||||
providers as {
|
||||
name: string;
|
||||
icon: string;
|
||||
id: string;
|
||||
}[],
|
||||
);
|
||||
};
|
||||
59
composables/ParsedContent.ts
Normal file
59
composables/ParsedContent.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { Account } from "~/types/mastodon/account";
|
||||
import type { Emoji } from "~/types/mastodon/emoji";
|
||||
import MentionComponent from "../components/social-elements/notes/mention.vue";
|
||||
import { renderToString } from "vue/server-renderer";
|
||||
|
||||
/**
|
||||
* Takes in an HTML string, parses emojis and returns a reactive object with the parsed content.
|
||||
* @param content String of HTML content to parse
|
||||
* @param emojis Array of emojis to parse
|
||||
* @returns Reactive object with the parsed content
|
||||
*/
|
||||
export const useParsedContent = async (
|
||||
content: string,
|
||||
emojis: Emoji[],
|
||||
mentions: Account[],
|
||||
): Promise<Ref<string>> => {
|
||||
const contentHtml = document.createElement("div");
|
||||
contentHtml.innerHTML = content;
|
||||
|
||||
// Replace emoji shortcodes with images
|
||||
const paragraphs = contentHtml.querySelectorAll("p");
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
paragraph.innerHTML = paragraph.innerHTML.replace(
|
||||
/:([a-z0-9_-]+):/g,
|
||||
(match, emoji) => {
|
||||
const emojiData = emojis.find((e) => e.shortcode === emoji);
|
||||
if (!emojiData) {
|
||||
return match;
|
||||
}
|
||||
const image = document.createElement("img");
|
||||
image.src = emojiData.url;
|
||||
image.alt = `:${emoji}:`;
|
||||
image.title = emojiData.shortcode;
|
||||
image.className =
|
||||
"h-6 align-text-bottom inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out";
|
||||
return image.outerHTML;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Replace links containing mentions with interactive mentions
|
||||
const links = contentHtml.querySelectorAll("a");
|
||||
|
||||
for (const link of links) {
|
||||
const mention = mentions.find((m) => link.textContent === `@${m.acct}`);
|
||||
if (!mention) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedMention = h(MentionComponent);
|
||||
renderedMention.props = {
|
||||
account: mention,
|
||||
};
|
||||
|
||||
link.outerHTML = await renderToString(renderedMention);
|
||||
}
|
||||
return ref(contentHtml.innerHTML);
|
||||
};
|
||||
20
composables/ResolveMentions.ts
Normal file
20
composables/ResolveMentions.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
import type { Account } from "~/types/mastodon/account";
|
||||
import type { Mention } from "~/types/mastodon/mention";
|
||||
|
||||
export const useResolveMentions = async (
|
||||
mentions: Mention[],
|
||||
client: Mastodon | null,
|
||||
): Promise<Ref<Account[]>> => {
|
||||
if (!client) {
|
||||
return ref([]);
|
||||
}
|
||||
return ref(
|
||||
await Promise.all(
|
||||
mentions.map(async (mention) => {
|
||||
const response = await client.getAccount(mention.id);
|
||||
return response.data;
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
export const useConfig = async () => {
|
||||
let host =
|
||||
useRequestHeader("X-Forwarded-Host") ??
|
||||
useRuntimeConfig().public.apiHost;
|
||||
|
||||
if (!host?.includes("http")) {
|
||||
// On server, this will be some kind of localhost
|
||||
host = `http://${host}`;
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "No X-Forwarded-Host header found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
http: {
|
||||
url: host,
|
||||
base_url: host,
|
||||
},
|
||||
}; /* await fetch(new URL("/api/_fe/config", host)).then((res) =>
|
||||
res.json(),
|
||||
); */
|
||||
};
|
||||
86
composables/usePublicTimeline.ts
Normal file
86
composables/usePublicTimeline.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import type { Mastodon } from "megalodon";
|
||||
import type { Status } from "~/types/mastodon/status";
|
||||
|
||||
export const usePublicTimeline = (
|
||||
client: Mastodon | null,
|
||||
options: MaybeRef<
|
||||
Partial<{
|
||||
only_media: boolean;
|
||||
max_id: string;
|
||||
since_id: string;
|
||||
min_id: string;
|
||||
limit: number;
|
||||
}>
|
||||
>,
|
||||
): {
|
||||
timeline: Ref<Status[]>;
|
||||
loadNext: () => Promise<void>;
|
||||
loadPrev: () => Promise<void>;
|
||||
} => {
|
||||
if (!client) {
|
||||
return {
|
||||
timeline: ref([]),
|
||||
loadNext: async () => {},
|
||||
loadPrev: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const fetchedNotes = ref<Status[]>([]);
|
||||
const fetchedNoteIds = new Set<string>();
|
||||
let nextMaxId: string | undefined = undefined;
|
||||
let prevMinId: string | undefined = undefined;
|
||||
|
||||
const loadNext = async () => {
|
||||
const response = await client.getPublicTimeline({
|
||||
...ref(options).value,
|
||||
max_id: nextMaxId,
|
||||
limit: useConfig().NOTES_PER_PAGE,
|
||||
});
|
||||
|
||||
const newNotes = response.data.filter(
|
||||
(note) => !fetchedNoteIds.has(note.id),
|
||||
);
|
||||
if (newNotes.length > 0) {
|
||||
fetchedNotes.value = [...fetchedNotes.value, ...newNotes];
|
||||
nextMaxId = newNotes[newNotes.length - 1].id;
|
||||
for (const note of newNotes) {
|
||||
fetchedNoteIds.add(note.id);
|
||||
}
|
||||
} else {
|
||||
nextMaxId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPrev = async () => {
|
||||
const response = await client.getPublicTimeline({
|
||||
...ref(options).value,
|
||||
min_id: prevMinId,
|
||||
limit: useConfig().NOTES_PER_PAGE,
|
||||
});
|
||||
|
||||
const newNotes = response.data.filter(
|
||||
(note) => !fetchedNoteIds.has(note.id),
|
||||
);
|
||||
if (newNotes.length > 0) {
|
||||
fetchedNotes.value = [...newNotes, ...fetchedNotes.value];
|
||||
prevMinId = newNotes[0].id;
|
||||
for (const note of newNotes) {
|
||||
fetchedNoteIds.add(note.id);
|
||||
}
|
||||
} else {
|
||||
prevMinId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => ref(options).value,
|
||||
async ({ max_id, min_id }) => {
|
||||
nextMaxId = max_id;
|
||||
prevMinId = min_id;
|
||||
await loadNext();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return { timeline: fetchedNotes, loadNext, loadPrev };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue