fix navigation between statuses

This commit is contained in:
sam 2023-12-21 15:14:07 +01:00
parent 14e58c1b3d
commit c8478df21c
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
14 changed files with 1096 additions and 1489 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
pnpm-lock.yaml

View file

@ -16,12 +16,12 @@
"@fontsource/fira-sans": "^5.0.18", "@fontsource/fira-sans": "^5.0.18",
"@tabler/icons-vue": "^2.44.0", "@tabler/icons-vue": "^2.44.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"classnames": "^2.3.2",
"domhandler": "^5.0.3", "domhandler": "^5.0.3",
"flowbite": "^2.2.1", "flowbite": "^2.2.1",
"flowbite-vue": "^0.1.2", "flowbite-vue": "^0.1.2",
"htmlparser2": "^9.0.0", "htmlparser2": "^9.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"sass": "^1.69.5",
"swrv": "^1.0.4", "swrv": "^1.0.4",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "9", "vue-i18n": "9",
@ -43,6 +43,7 @@
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"sass": "^1.69.5",
"tailwindcss": "^3.3.7", "tailwindcss": "^3.3.7",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"vite": "^5.0.10", "vite": "^5.0.10",

File diff suppressed because it is too large Load diff

5
src/assets/mfm.scss Normal file
View file

@ -0,0 +1,5 @@
.mfm {
&._mfm_x2_ {
font-size: 200%;
}
}

View file

@ -1,16 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import classNames from "classnames";
import type Activity from "@/lib/api/entities/activity"; import type Activity from "@/lib/api/entities/activity";
import VulStatusInfo from "./VulStatusInfo.vue"; import VulStatusInfo from "./VulStatusInfo.vue";
import VulStatusContent from "./VulStatusContent.vue"; import VulStatusContent from "./VulStatusContent.vue";
import VulStatusButtons from "./VulStatusButtons.vue"; import VulStatusButtons from "./VulStatusButtons.vue";
defineProps<{ status: Activity }>(); const props = defineProps<{ status: Activity, current?: boolean }>();
const classes = classNames("max-w-3xl flex flex-col m-2 py-2 rounded-md", {
"bg-gray-600 dark:bg-gray-600": props.current,
})
</script> </script>
<template> <template>
<div class="flex flex-col m-2"> <div :class="classes">
<VulStatusInfo :status="status" /> <VulStatusInfo :status="status" />
<VulStatusContent :status="status" /> <VulStatusContent :status="status" />
<VulStatusButtons :status="status" /> <VulStatusButtons :status="status" :current="current" />
</div> </div>
</template> </template>

View file

@ -1,26 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import classNames from "classnames";
import { IconDotsVertical, IconMessage, IconPlus, IconQuote, IconRepeat, IconStar } from "@tabler/icons-vue"; import { IconDotsVertical, IconMessage, IconPlus, IconQuote, IconRepeat, IconStar } from "@tabler/icons-vue";
import type Activity from "@/lib/api/entities/activity"; import type Activity from "@/lib/api/entities/activity";
defineProps<{ status: Activity }>(); const props = defineProps<{ status: Activity, current?: boolean }>();
const classes = classNames("pt-1 mt-4 flex border-t", {
"dark:border-gray-600": props.current,
"dark:border-gray-700": !props.current,
})
</script> </script>
<template> <template>
<div class="flex"> <div :class="classes">
<ul class="flex space-x-4"> <ul class="my-2 mx-4 flex space-x-4">
<li> <li>
<span v-if="status.replies_count">{{ status.replies_count }}</span> <span v-if="status.replies_count" class="me-2">{{ status.replies_count }}</span>
<IconMessage class="inline" /> <IconMessage class="inline" />
</li> </li>
<li> <li>
<span v-if="status.reblogs_count">{{ status.reblogs_count }}</span> <span v-if="status.reblogs_count" class="me-2">{{ status.reblogs_count }}</span>
<IconRepeat class="inline" /> <IconRepeat class="inline" />
</li> </li>
<li> <li>
<IconQuote class="inline" /> <IconQuote class="inline" />
</li> </li>
<li> <li>
<span v-if="status.favourites_count">{{ status.favourites_count }}</span> <span v-if="status.favourites_count" class="me-2">{{ status.favourites_count }}</span>
<IconStar class="inline" /> <IconStar class="inline" />
</li> </li>
<li> <li>

View file

@ -13,7 +13,7 @@ const isCollapsed = ref(!!status.spoiler_text);
</script> </script>
<template> <template>
<div class="flex flex-col"> <div class="mx-2 flex flex-col">
<div v-if="status.spoiler_text"> <div v-if="status.spoiler_text">
<strong v-html="status.spoiler_text"></strong> <strong v-html="status.spoiler_text"></strong>
<FwbButton class="mx-2" color="alternative" size="sm" @click="() => (isCollapsed = !isCollapsed)"> <FwbButton class="mx-2" color="alternative" size="sm" @click="() => (isCollapsed = !isCollapsed)">

View file

@ -34,7 +34,7 @@ const statusScopeIcon = ({ visibility }: Activity) => {
</script> </script>
<template> <template>
<div class="flex flex-row"> <div class="mx-2 flex flex-row">
<div class="flex flex-row flex-grow"> <div class="flex flex-row flex-grow">
<RouterLink :to="`/@${status.account.acct}`"> <RouterLink :to="`/@${status.account.acct}`">
<img <img

View file

@ -25,15 +25,7 @@ export default function HTMLContent(props: Props) {
const dom = htmlparser2.parseDocument(content); const dom = htmlparser2.parseDocument(content);
const elements = parseElement(dom, props.emoji, props.tags || [], props.mentions || []); const elements = parseElement(dom, props.emoji, props.tags || [], props.mentions || []);
return <>{elements}</>;
return (
<>
<code>
<pre>{dom}</pre>
</code>
{elements}
</>
);
} }
/** Create a custom emoji element and return its string representation. /** Create a custom emoji element and return its string representation.
@ -57,8 +49,6 @@ function parseElement(elem: ChildNode, emoji: CustomEmoji[], tags: Tag[], mentio
// text node, just add it to the output unmodified // text node, just add it to the output unmodified
out.push(<>{elem.data}</>); out.push(<>{elem.data}</>);
} else if (elem instanceof Element) { } else if (elem instanceof Element) {
console.log(elem.name, elem.attribs);
switch (elem.name) { switch (elem.name) {
case "x-emoji": case "x-emoji":
// Turn <x-emoji> into a VulEmoji component // Turn <x-emoji> into a VulEmoji component
@ -70,8 +60,8 @@ function parseElement(elem: ChildNode, emoji: CustomEmoji[], tags: Tag[], mentio
if ("class" in elem.attribs && elem.attribs.class === "h-card") { if ("class" in elem.attribs && elem.attribs.class === "h-card") {
// if so, parse that mention and add a component for it // if so, parse that mention and add a component for it
if (elem.children.length === 0) throw "Invalid mention, span with type h-card doesn't have child"; if (elem.children.length === 0) throw "Invalid mention, span with type h-card doesn't have child";
const userId = (elem.children[0] as Element).attribs["data-user"]; const userUrl = (elem.children[0] as Element).attribs["href"];
const user = mentions.find((m) => m.id === userId); const user = mentions.find((m) => m.url === userUrl);
if (!user) throw "Invalid mention, user is not found in Status.mentions"; if (!user) throw "Invalid mention, user is not found in Status.mentions";
// add the mention component // add the mention component
out.push(<VulMention mention={user} />); out.push(<VulMention mention={user} />);

View file

@ -11,6 +11,6 @@ defineProps<{ emoji: CustomEmoji }>();
<style> <style>
.emoji { .emoji {
display: inline; display: inline;
height: 38px; height: 2em;
} }
</style> </style>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import type Activity from "@/lib/api/entities/activity";
import VulStatus from "@/components/status/VulStatus.vue";
defineProps<{
ancestors?: Activity[];
descendants?: Activity[];
currentStatus: Activity;
}>();
</script>
<template>
<template v-if="ancestors">
<VulStatus v-for="status in ancestors" :status="status" :key="status.id" />
</template>
<VulStatus :status="currentStatus" :key="currentStatus.id" :current="true" />
<template v-if="descendants">
<VulStatus v-for="status in descendants" :status="status" :key="status.id" />
</template>
</template>

View file

@ -19,6 +19,11 @@ export default interface Activity {
replies_count: number; replies_count: number;
} }
export interface ActivityContext {
ancestors: Activity[];
descendants: Activity[];
}
export interface ActivityAkkoma { export interface ActivityAkkoma {
source: ActivityAkkomaSource; source: ActivityAkkomaSource;
} }

View file

@ -1,4 +1,5 @@
import "./assets/style.css"; import "./assets/style.css";
import "./assets/mfm.scss";
import { createApp } from "vue"; import { createApp } from "vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";

View file

@ -3,18 +3,23 @@ import { ref, watch } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import useSWRV from "swrv"; import useSWRV from "swrv";
import type Activity from "@/lib/api/entities/activity"; import type Activity from "@/lib/api/entities/activity";
import type { ActivityContext } from "@/lib/api/entities/activity";
import apiFetch from "@/lib/api-fetch"; import apiFetch from "@/lib/api-fetch";
import watchTitle from "@/lib/title";
import { FwbSpinner } from "flowbite-vue"; import { FwbSpinner } from "flowbite-vue";
import VulStatus from "@/components/status/VulStatus.vue"; import StatusTree from "@/components/status_tree/StatusTree.vue";
import watchTitle from "@/lib/title";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const username = ref(route.params.username); const username = ref(route.params.username);
const statusId = ref(route.params.statusId); const statusId = ref(route.params.statusId);
const { data, error } = useSWRV(() => `/api/v1/statuses/${statusId.value}`, apiFetch<Activity>); const { data: status, error: statusError } = useSWRV(() => `/api/v1/statuses/${statusId.value}`, apiFetch<Activity>);
const { data: context, error: contextError } = useSWRV(
() => `/api/v1/statuses/${statusId.value}/context`,
apiFetch<ActivityContext>,
);
// update username/status ID whenever we navigate to another page // update username/status ID whenever we navigate to another page
watch( watch(
@ -27,9 +32,9 @@ watch(
// always have the correct username in the URL // always have the correct username in the URL
watch( watch(
() => ({ activity: data.value, username: route.params.username }), () => ({ activity: status.value, username: route.params.username, statusId: statusId.value }),
({ activity, username }) => { ({ activity, username, statusId }) => {
if (activity && activity.account.acct !== username) { if (activity && activity.id === statusId && activity.account.acct !== username) {
router.push({ router.push({
name: "user-status", name: "user-status",
params: { username: activity.account.acct, statusId: activity.id }, params: { username: activity.account.acct, statusId: activity.id },
@ -43,11 +48,12 @@ watchTitle((activity) => {
const user = activity.account.display_name; const user = activity.account.display_name;
let text = activity.spoiler_text || activity.content || "N/A"; let text = activity.spoiler_text || activity.content || "N/A";
return `${user}: "${text.length > 29 ? text.slice(0, 30) + "…" : text}"`; return `${user}: "${text.length > 29 ? text.slice(0, 30) + "…" : text}"`;
}, data); }, status);
</script> </script>
<!-- TODO: replace the FwbSpinner with an actual loading component -->
<template> <template>
<div v-if="error">Failed to load: {{ error }}</div> <div v-if="statusError || contextError">Failed to load: {{ statusError || contextError }}</div>
<FwbSpinner v-else-if="!data" size="10" /> <FwbSpinner v-else-if="!status" size="10" />
<VulStatus v-else :status="data" /> <StatusTree v-else :ancestors="context?.ancestors" :descendants="context?.descendants" :current-status="status" />
</template> </template>