feat: ♻️ Change code to build in static mode, add timelines, profiles and notes, new design

This commit is contained in:
Jesse Wierzbinski 2024-04-21 21:38:51 -10:00
parent 9edfd5ac2d
commit acd50ece9b
No known key found for this signature in database
75 changed files with 1603 additions and 549 deletions

12
composables/Account.ts Normal file
View 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;
};

View 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
View file

@ -0,0 +1,3 @@
export const useBaseUrl = () => {
return ref(useRuntimeConfig().public.apiHost ?? useRequestURL().origin);
};

12
composables/Config.ts Normal file
View 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
View 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
View 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
View 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;
};

View 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;
}[],
);
};

View 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);
};

View 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;
}),
),
);
};

View file

@ -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(),
); */
};

View 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 };
};