diff --git a/frontend/packages/app/src/lib/components/Button.tsx b/frontend/packages/app/src/lib/components/Button.tsx index 3af7b202..07d11d06 100644 --- a/frontend/packages/app/src/lib/components/Button.tsx +++ b/frontend/packages/app/src/lib/components/Button.tsx @@ -10,17 +10,20 @@ interface ButtonProps extends React.ButtonHTMLAttributes { danger?: boolean; nav?: boolean; to?: string; + realLink?: boolean; ref?: RefObject; children: React.ReactNode; } -function Button({wide, thin, center, primary, danger, nav, to, ref, children, ...props}: ButtonProps) { +function Button({wide, thin, center, primary, danger, nav, to, realLink, ref, children, ...props}: ButtonProps) { const navigate = useNavigate(); if (to !== undefined) { - props.onClick = (e) => { - e.preventDefault() // annoying hack - navigate({to: to}) + if (!realLink) { + props.onClick = (e) => { + e.preventDefault() // annoying hack + navigate({to: to}) + } } return ( diff --git a/frontend/packages/app/src/lib/components/Container.scss b/frontend/packages/app/src/lib/components/Container.scss index 9bc84346..c1bd95a8 100644 --- a/frontend/packages/app/src/lib/components/Container.scss +++ b/frontend/packages/app/src/lib/components/Container.scss @@ -10,6 +10,10 @@ } } + &.wrap { + flex-wrap: wrap; + } + &.center { width: 100%; align-items: center; diff --git a/frontend/packages/app/src/lib/components/Container.tsx b/frontend/packages/app/src/lib/components/Container.tsx index feeb0959..b6a2fbc4 100644 --- a/frontend/packages/app/src/lib/components/Container.tsx +++ b/frontend/packages/app/src/lib/components/Container.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import './Container.scss' function Container( - {gap, align, border, padding, clazz, fill, children, ...props}: + {gap, align, border, padding, clazz, fill, wrap, children, ...props}: { gap?: "sm" | "md" | "lg" | "xl" | undefined, align?: 'center' | 'left' | 'right' | 'horizontal' | 'horizontalCenter' | 'horizontalRight' | 'startHorizontal', @@ -10,13 +10,14 @@ function Container( padding?: string | undefined, clazz?: string, fill?: boolean, + wrap?: boolean, children: React.ReactNode, props?: never } ) { return (
diff --git a/frontend/packages/app/src/lib/components/DriveFile.scss b/frontend/packages/app/src/lib/components/DriveFile.scss index cd84a896..b5533970 100644 --- a/frontend/packages/app/src/lib/components/DriveFile.scss +++ b/frontend/packages/app/src/lib/components/DriveFile.scss @@ -13,11 +13,24 @@ background-color: var(--bg-3-50); } - img { + img, .preview { + display: flex; + align-items: center; + justify-content: center; + height: 115px; width: 115px; object-fit: cover; border-radius: var(--br-sm); + background-color: var(--bg-3); + color: var(--tx-3); + } + + .name { + max-width: 110px; + text-wrap: wrap; + word-break: break-all; + text-align: center; } } diff --git a/frontend/packages/app/src/lib/components/DriveFile.tsx b/frontend/packages/app/src/lib/components/DriveFile.tsx index ca7a56b3..62421df7 100644 --- a/frontend/packages/app/src/lib/components/DriveFile.tsx +++ b/frontend/packages/app/src/lib/components/DriveFile.tsx @@ -3,15 +3,56 @@ import {Api} from "aster-common"; import './DriveFile.scss' import Container from "./Container.tsx"; import Button from "./Button.tsx"; -import {IconPencil, IconTrash} from "@tabler/icons-react"; +import { + IconCoffee, + IconFile, + IconFileZip, + IconPencil, + IconQuestionMark, + IconTrash, + IconVideo +} from "@tabler/icons-react"; import * as React from "react"; function DriveFile({data}: { data: common.DriveFile }) { const [hidden, setHidden] = React.useState(false); + function renderPreview() { + const type = data.type + if (type.startsWith("image")) { + return {data.alt}/ + } else if (type.startsWith("video")) { + return
+ +
+ } else if (type.startsWith("application/zip")) { + return
+ +
+ } else if (type.startsWith("application/java-archive")) { + return
+ +
+ } else if (type.startsWith("application") || type.startsWith("text")) { + return
+ +
+ } else { + return
+ +
+ } + } + + function renderName() { + let split = data?.src?.split("/") + return {split[split.length - 1]} + } + return hidden ? null : (
- {data.alt}/ + {renderPreview()} + {renderName()} +
) diff --git a/frontend/packages/app/src/lib/components/Timeline.tsx b/frontend/packages/app/src/lib/components/Timeline.tsx index 804d9c65..1ca80916 100644 --- a/frontend/packages/app/src/lib/components/Timeline.tsx +++ b/frontend/packages/app/src/lib/components/Timeline.tsx @@ -1,46 +1,72 @@ import * as React from "react"; +import {useRef} from "react"; import './Timeline.scss'; -import Container from "./Container.tsx"; +import {randomString} from "aster-common" +import type {DefinedUseInfiniteQueryResult} from "@tanstack/react-query"; +import Container from "./Container"; +import Loading from "./Loading.tsx"; function Timeline( - {data, Component}: - { data?: any[], Component: any } + {query, Component, grid = false}: + { query?: DefinedUseInfiniteQueryResult, Component: any, grid?: boolean } ) { - React.useEffect(() => { - render() - }) + const intersectionRef = useRef(null) + const intersectionId = randomString() - let random = Math.floor(Math.random() * (Math.ceil(1) - Math.floor(100000))); + let noMore = false - let timeline: any[] = [] + const observer = new IntersectionObserver(async (entries) => { + if (!noMore) { + console.log("Intersection observed") + if (entries[0]?.isIntersecting) query?.fetchNextPage() + } + }, { + threshold: 0.8 + }); - function clear() { - timeline = [] - } + React.useEffect(() => { + if (intersectionRef?.current) + observer.observe(intersectionRef.current); + }) - function render() { - clear() - data?.forEach((item) => { - random++ - timeline.push( - - + function renderBaseTimeline() { + if (query?.data && query.data?.pages && query.data.pages.length > 0) { + return ( + query?.data?.pages?.map((items) => ( + (items && items.length > 0) ? + items?.map((item) => ( + (item ? + : null) + )) : renderNone() + )) ) - }) + } else { + return renderNone() + } } - render() + function renderNone() { + noMore = true + return ( + + Nothing more to show... + + ) + } - return ( + return <>
- {timeline.length > 0 ? timeline : ( - - Nothing to show... - - )} + {grid ? + {renderBaseTimeline()} + : renderBaseTimeline()} +
+
+ + {query?.isFetchingNextPage ? : null} +
- ) + } export default Timeline diff --git a/frontend/packages/app/src/lib/components/widgets/Navigation.tsx b/frontend/packages/app/src/lib/components/widgets/Navigation.tsx index 35b10ec3..1a3ad7f6 100644 --- a/frontend/packages/app/src/lib/components/widgets/Navigation.tsx +++ b/frontend/packages/app/src/lib/components/widgets/Navigation.tsx @@ -6,7 +6,6 @@ import { IconDashboard, IconDots, IconFolder, - IconHash, IconHome, IconSearch, IconSettings, @@ -31,12 +30,6 @@ function NavigationWidget() { Notifications -
  • - -
  • - @@ -85,4 +78,4 @@ function NavigationWidget() { ) } -export default NavigationWidget; \ No newline at end of file +export default NavigationWidget; diff --git a/frontend/packages/app/src/lib/queryClient.ts b/frontend/packages/app/src/lib/queryClient.ts index 6409f249..9584b90c 100644 --- a/frontend/packages/app/src/lib/queryClient.ts +++ b/frontend/packages/app/src/lib/queryClient.ts @@ -5,7 +5,7 @@ const queryClient = new QueryClient({ queries: { retry: 2, retryDelay: 5000, - refetchOnWindowFocus: false, + refetchOnWindowFocus: false } } }); diff --git a/frontend/packages/app/src/main.tsx b/frontend/packages/app/src/main.tsx index 685e5fc9..962d47d4 100644 --- a/frontend/packages/app/src/main.tsx +++ b/frontend/packages/app/src/main.tsx @@ -1,3 +1,4 @@ +import "preact/debug" import {StrictMode} from 'react' import {createRoot} from 'react-dom/client' import {RouterProvider} from "@tanstack/react-router"; diff --git a/frontend/packages/app/src/routes/about.tsx b/frontend/packages/app/src/routes/about.tsx index 28f5cb89..f0aa273d 100644 --- a/frontend/packages/app/src/routes/about.tsx +++ b/frontend/packages/app/src/routes/about.tsx @@ -22,19 +22,12 @@ function RouteComponent() { title={"About"} /> - {"Aster + {"Aster

    Aster {data?.version?.aster}

    - {data?.plugins?.length > 0 ? ( -
    - Plugins - {JSON.stringify(data?.plugins)} -
    - ) : null} -

    Kotlin {data?.version?.kotlin}
    Runtime {data?.version?.java}
    diff --git a/frontend/packages/app/src/routes/drive.tsx b/frontend/packages/app/src/routes/drive.tsx index ec4a6295..c5afb21e 100644 --- a/frontend/packages/app/src/routes/drive.tsx +++ b/frontend/packages/app/src/routes/drive.tsx @@ -2,24 +2,28 @@ import {createFileRoute} from "@tanstack/react-router"; import PageHeader from "../lib/components/PageHeader.tsx"; import {IconFolder, IconPlus} from "@tabler/icons-react"; import PageWrapper from "../lib/components/PageWrapper.tsx"; -import {useQuery} from "@tanstack/react-query"; +import {useInfiniteQuery} from "@tanstack/react-query"; import localstore from "../lib/utils/localstore.ts"; import DriveFile from "../lib/components/DriveFile.tsx"; -import Container from "../lib/components/Container.tsx"; import Loading from "../lib/components/Loading.tsx"; import Error from "../lib/components/Error.tsx"; import Button from "../lib/components/Button.tsx"; import type {ChangeEvent} from "react"; import {Api} from 'aster-common' +import Timeline from "../lib/components/Timeline.tsx"; export const Route = createFileRoute('/drive')({ component: RouteComponent, }) function RouteComponent() { - const {data, error, isPending, isFetching, refetch} = useQuery({ + const query = useInfiniteQuery({ queryKey: [`drive_${localstore.getSelf()?.id}`], - queryFn: () => Api.getDrive(), + queryFn: ({pageParam}) => Api.getDrive(pageParam), + initialPageParam: undefined, + getNextPageParam: (lastPage) => { + return lastPage ? lastPage?.at(-1)?.createdAt : undefined + } }); function upload(e: ChangeEvent) { @@ -53,16 +57,12 @@ function RouteComponent() { /> - {isPending || isFetching ? ( + {query.isPending ? ( - ) : error ? ( - + ) : query.error ? ( + ) : ( - - {data?.map((file) => ( - - ))} - + )} diff --git a/frontend/packages/app/src/routes/index.tsx b/frontend/packages/app/src/routes/index.tsx index 4e793f6c..59737c5a 100644 --- a/frontend/packages/app/src/routes/index.tsx +++ b/frontend/packages/app/src/routes/index.tsx @@ -5,7 +5,7 @@ import {IconChartBubble, IconHome, IconPlanet, IconUsers} from "@tabler/icons-re import localstore from "../lib/utils/localstore.ts"; import Timeline from "../lib/components/Timeline.tsx"; import Note from "../lib/components/Note.tsx"; -import {useQuery} from "@tanstack/react-query"; +import {useInfiniteQuery} from "@tanstack/react-query"; import {useState} from "react"; import Tab from "../lib/components/Tab.tsx"; import Loading from "../lib/components/Loading.tsx"; @@ -23,16 +23,20 @@ function RouteComponent() { let previousTimeline = localstore.getParsed('timeline') let [timeline, setTimeline] = useState((previousTimeline === undefined) ? "home" : previousTimeline); - const {isPending, error, data, isFetching, refetch} = useQuery({ - queryKey: ['timeline'], - queryFn: async () => Api.getTimeline(timeline), - }) + const query = useInfiniteQuery({ + queryKey: [`timeline`], + queryFn: ({pageParam}) => Api.getTimeline(timeline, pageParam), + initialPageParam: undefined, + getNextPageParam: (lastPage) => { + return lastPage ? lastPage?.at(-1)?.createdAt : undefined + } + }); function updateTimeline(timeline: string) { setTimeline(timeline); localstore.set('timeline', timeline); setTimeout(async () => { - await refetch() + await query.refetch() }, 100) } @@ -76,12 +80,12 @@ function RouteComponent() { - {isPending || isFetching ? ( + {query.isPending ? ( - ) : error ? ( - + ) : query.error ? ( + ) : ( - + )} diff --git a/frontend/packages/app/src/routes/note/$noteId.tsx b/frontend/packages/app/src/routes/note/$noteId.tsx index 1cf9003a..a507d744 100644 --- a/frontend/packages/app/src/routes/note/$noteId.tsx +++ b/frontend/packages/app/src/routes/note/$noteId.tsx @@ -41,13 +41,13 @@ function RouteComponent() { case 1: return ( <> - + ) case 2: return ( <> - + ) } diff --git a/frontend/packages/app/src/routes/notifications.tsx b/frontend/packages/app/src/routes/notifications.tsx index 31c3e669..796767a6 100644 --- a/frontend/packages/app/src/routes/notifications.tsx +++ b/frontend/packages/app/src/routes/notifications.tsx @@ -3,7 +3,7 @@ import PageHeader from "../lib/components/PageHeader.tsx"; import {IconBell} from "@tabler/icons-react"; import PageWrapper from "../lib/components/PageWrapper.tsx"; import localstore from "../lib/utils/localstore.ts"; -import {useQuery} from "@tanstack/react-query"; +import {useInfiniteQuery} from "@tanstack/react-query"; import Loading from "../lib/components/Loading.tsx"; import Error from "../lib/components/Error.tsx"; import Timeline from "../lib/components/Timeline.tsx"; @@ -18,10 +18,14 @@ function RouteComponent() { const token = localstore.getParsed('token'); if (token) { - const {isPending, error, data, isFetching, refetch} = useQuery({ - queryKey: ['Notifications'], - queryFn: async () => await Api.getNotifications(), - }) + const query = useInfiniteQuery({ + queryKey: [`notifications`], + queryFn: ({pageParam}) => Api.getNotifications(pageParam), + initialPageParam: undefined, + getNextPageParam: (lastPage) => { + return lastPage ? lastPage?.at(-1)?.createdAt : undefined + } + }); return <> }> @@ -44,12 +48,12 @@ function RouteComponent() { */} - {isPending || isFetching ? ( + {query.isPending ? ( - ) : error ? ( - + ) : query.error ? ( + ) : ( - + )} diff --git a/frontend/packages/app/src/routes/search.tsx b/frontend/packages/app/src/routes/search.tsx index 58714b82..2f1b327d 100644 --- a/frontend/packages/app/src/routes/search.tsx +++ b/frontend/packages/app/src/routes/search.tsx @@ -6,10 +6,6 @@ import Container from "../lib/components/Container.tsx"; import Button from "../lib/components/Button.tsx"; import Input from "../lib/components/Input.tsx"; import {useForm} from "@tanstack/react-form"; -import Timeline from "../lib/components/Timeline.tsx"; -import {useState} from "react"; -import Search from "../lib/components/Search.tsx"; -import * as Common from 'aster-common' import {Api} from 'aster-common' export const Route = createFileRoute('/search')({ @@ -17,15 +13,12 @@ export const Route = createFileRoute('/search')({ }) function RouteComponent() { - const [data, setData] = useState(new Common.SearchResults(false, [])) - const form = useForm({ defaultValues: { query: "", }, onSubmit: async (values) => { Api.search(values.value.query).then((data) => { - setData(data); }); } }); @@ -72,7 +65,7 @@ function RouteComponent() { - + {/* */} diff --git a/frontend/packages/app/vite.config.ts b/frontend/packages/app/vite.config.ts index 9cbe5839..8de39d16 100644 --- a/frontend/packages/app/vite.config.ts +++ b/frontend/packages/app/vite.config.ts @@ -37,6 +37,10 @@ export default defineConfig({ target: 'http://localhost:9978', changeOrigin: false, }, + '/favicon.ico': { + target: 'http://localhost:9978', + changeOrigin: false, + }, '/uikit': { target: 'http://localhost:9978', changeOrigin: false, diff --git a/src/main/kotlin/site/remlit/aster/route/api/TimelineRoutes.kt b/src/main/kotlin/site/remlit/aster/route/api/TimelineRoutes.kt index 9f6a4f2e..2ba981a5 100644 --- a/src/main/kotlin/site/remlit/aster/route/api/TimelineRoutes.kt +++ b/src/main/kotlin/site/remlit/aster/route/api/TimelineRoutes.kt @@ -53,7 +53,7 @@ internal object TimelineRoutes { val local = call.request.queryParameters["local"]?.toBoolean() ?: true val following = RelationshipService.getFollowing(authenticatedUser).map { it.id } - var where = (NoteTable.createdAt less since) and ( + var where = ( (NoteTable.visibility inList listOf( Visibility.Public, Visibility.Unlisted, @@ -67,7 +67,7 @@ internal object TimelineRoutes { and (UserTable.host eq null))) val notes = NoteService.getMany( - where = where, + where = (where) and (NoteTable.createdAt less since), take = take ) diff --git a/src/main/kotlin/site/remlit/aster/service/TimelineService.kt b/src/main/kotlin/site/remlit/aster/service/TimelineService.kt index c315d1b5..49ecc361 100644 --- a/src/main/kotlin/site/remlit/aster/service/TimelineService.kt +++ b/src/main/kotlin/site/remlit/aster/service/TimelineService.kt @@ -3,6 +3,9 @@ package site.remlit.aster.service import kotlinx.datetime.LocalDateTime import site.remlit.aster.model.Configuration import site.remlit.aster.model.Service +import site.remlit.aster.util.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.Instant /** * Service for timeline related utilities. @@ -40,7 +43,7 @@ object TimelineService : Service { * */ @JvmStatic fun normalizeSince(since: String?): LocalDateTime { - val now = TimeService.now().toString() - return LocalDateTime.parse(since ?: now) + return (if (since != null) Instant.parse(since) else Clock.System.now()) + .toLocalDateTime() } }