fix navigation between statuses
This commit is contained in:
parent
14e58c1b3d
commit
c8478df21c
14 changed files with 1096 additions and 1489 deletions
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pnpm-lock.yaml
|
|
@ -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",
|
||||||
|
|
2385
pnpm-lock.yaml
2385
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
5
src/assets/mfm.scss
Normal file
5
src/assets/mfm.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.mfm {
|
||||||
|
&._mfm_x2_ {
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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} />);
|
||||||
|
|
|
@ -11,6 +11,6 @@ defineProps<{ emoji: CustomEmoji }>();
|
||||||
<style>
|
<style>
|
||||||
.emoji {
|
.emoji {
|
||||||
display: inline;
|
display: inline;
|
||||||
height: 38px;
|
height: 2em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
20
src/components/status_tree/StatusTree.vue
Normal file
20
src/components/status_tree/StatusTree.vue
Normal 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>
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue