diff --git a/leaf/ts/index.ts b/leaf/ts/index.ts index 1f1fce0..279a827 100644 --- a/leaf/ts/index.ts +++ b/leaf/ts/index.ts @@ -30,8 +30,8 @@ export const SubspaceSecretKeySchema = SubspaceIdSchema; export type PathSegment = | { Null: Unit } | { Bool: boolean } - | { Uint: number } - | { Int: number } + | { Uint: bigint | number } + | { Int: bigint | number } | { String: string } | { Bytes: number[] }; export const PathSegmentSchema = BorshSchema.Enum({ @@ -53,6 +53,8 @@ export function formatEntityPath(p: EntityPath): string { s += `/"${segment.String}"`; } else if ('Bytes' in segment) { s += `/base32:${base32Encode(new Uint8Array(segment.Bytes))}`; + } else if ('Uint' in segment) { + s += `/Uint:${segment.Uint.toString()}`; } else { throw 'TODO: implement formatting for other path segment types.'; } diff --git a/src/lib/leaf/index.ts b/src/lib/leaf/index.ts index 96353d2..5f4fd84 100644 --- a/src/lib/leaf/index.ts +++ b/src/lib/leaf/index.ts @@ -16,7 +16,8 @@ import { WebLinks, WeirdCustomDomain, WeirdPubpageTheme, - WeirdWikiPage + WeirdWikiPage, + WeirdWikiRevisionAuthor } from './profile'; /** The Leaf RPC client used to connect to our backend data server. */ @@ -79,6 +80,7 @@ export type KnownComponents = { weirdCustomDomain?: WeirdCustomDomain['value']; commonmark?: CommonMark['value']; weirdWikiPage?: WeirdWikiPage['value']; + weirdWikiRevisionAuthor?: WeirdWikiRevisionAuthor['value']; }; export async function loadKnownComponents(link: ExactLink): Promise { @@ -92,7 +94,8 @@ export async function loadKnownComponents(link: ExactLink): Promise ({ String: x }))] + path: [...link.path, ...pathSegments.map((x) => (typeof x == 'string' ? { String: x } : x))] }; } diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts new file mode 100644 index 0000000..2cb7a6f --- /dev/null +++ b/src/lib/utils/time.ts @@ -0,0 +1,7 @@ +export function dateToUnixTimestamp(date: Date): bigint { + return BigInt(Math.round((date?.getTime() || Date.now()) / 1000)); +} + +export function dateFromUnixTimestamp(timestamp: number | bigint): Date { + return new Date(Number(timestamp) * 1000); +} diff --git a/src/routes/(app)/[username]/[slug]/+page.server.ts b/src/routes/(app)/[username]/[slug]/+page.server.ts index c79119e..b840051 100644 --- a/src/routes/(app)/[username]/[slug]/+page.server.ts +++ b/src/routes/(app)/[username]/[slug]/+page.server.ts @@ -2,8 +2,8 @@ import type { Actions, PageServerLoad } from './$types'; import { WebLinks, WeirdWikiPage, + WeirdWikiRevisionAuthor, appendSubpath, - profileLinkById, profileLinkByUsername } from '$lib/leaf/profile'; import { error, fail, redirect } from '@sveltejs/kit'; @@ -13,6 +13,7 @@ import { leafClient } from '$lib/leaf'; import { CommonMark, Name } from 'leaf-proto/components'; import { Page } from '../types'; import { getSession } from '$lib/rauthy/server'; +import { dateToUnixTimestamp } from '$lib/utils/time'; export const load: PageServerLoad = async ({ params }): Promise<{ page: Page }> => { const username = parseUsername(params.username); @@ -92,17 +93,23 @@ export const actions = { newSlug = editorIsOwner ? data.slug : params.slug; const pageLink = appendSubpath(profileLink, newSlug); + const revisionLink = appendSubpath(pageLink, { Uint: dateToUnixTimestamp(new Date()) }); if (data.slug != params.slug) { await leafClient.del_entity(oldPageLink); } - await leafClient.update_components(pageLink, [ + const components = [ new Name(data.display_name), data.markdown.length > 0 ? new CommonMark(data.markdown) : CommonMark, data.links.length > 0 ? new WebLinks(data.links) : WebLinks, // non-owners are not allowed to change the wiki page status (editorIsOwner ? data.wiki : isWikiPage) ? new WeirdWikiPage() : WeirdWikiPage + ]; + await leafClient.update_components(pageLink, components); + await leafClient.update_components(revisionLink, [ + ...components, + new WeirdWikiRevisionAuthor(sessionInfo.user_id) ]); } catch (e: any) { return fail(500, { error: JSON.stringify(e) }); diff --git a/src/routes/(app)/[username]/[slug]/+page.svelte b/src/routes/(app)/[username]/[slug]/+page.svelte index b16dad9..5cc3b8f 100644 --- a/src/routes/(app)/[username]/[slug]/+page.svelte +++ b/src/routes/(app)/[username]/[slug]/+page.svelte @@ -92,8 +92,16 @@
By - {data.profile.display_name} + {data.profile.display_name}. + {#if data.page.wiki} + Wiki Page. + {/if} + See + Revisions.
{#if editingState.editing} diff --git a/src/routes/(app)/[username]/[slug]/revisions/+page.server.ts b/src/routes/(app)/[username]/[slug]/revisions/+page.server.ts new file mode 100644 index 0000000..9555f73 --- /dev/null +++ b/src/routes/(app)/[username]/[slug]/revisions/+page.server.ts @@ -0,0 +1,38 @@ +import type { PageServerLoad } from './$types'; +import { appendSubpath, profileLinkByUsername } from '$lib/leaf/profile'; +import { error, redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; +import { parseUsername } from '$lib/utils/username'; +import { leafClient } from '$lib/leaf'; + +export const load: PageServerLoad = async ({ + params +}): Promise<{ revisions: (number | bigint)[] }> => { + const username = parseUsername(params.username); + if (username.domain == env.PUBLIC_DOMAIN) { + return redirect(302, `/${username.name}/${params.slug}`); + } + const fullUsername = `${username.name}@${username.domain || env.PUBLIC_DOMAIN}`; + const profileLink = await profileLinkByUsername(fullUsername); + + if (!profileLink) return error(404, `User not found: ${fullUsername}`); + const pageLink = appendSubpath(profileLink, params.slug); + + const links = await leafClient.list_entities(pageLink); + const revisions = []; + for (const link of links) { + const last = link.path[link.path.length - 1]; + if ('Uint' in last) { + revisions.push(last.Uint); + } + } + + revisions.sort(); + // Remove the last revision because it's the same as the current one + revisions.pop(); + revisions.reverse(); + + return { + revisions + }; +}; diff --git a/src/routes/(app)/[username]/[slug]/revisions/+page.svelte b/src/routes/(app)/[username]/[slug]/revisions/+page.svelte new file mode 100644 index 0000000..c864a43 --- /dev/null +++ b/src/routes/(app)/[username]/[slug]/revisions/+page.svelte @@ -0,0 +1,40 @@ + + +
+

Revisions

+ +

+ {$page.params.username} / {$page.params.slug} +

+ + +
diff --git a/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.server.ts b/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.server.ts new file mode 100644 index 0000000..19d8fa7 --- /dev/null +++ b/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.server.ts @@ -0,0 +1,70 @@ +import type { Actions, PageServerLoad } from './$types'; +import { + Username, + WebLinks, + WeirdWikiPage, + WeirdWikiRevisionAuthor, + appendSubpath, + getProfileById, + profileLinkById, + profileLinkByUsername +} from '$lib/leaf/profile'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; +import { parseUsername } from '$lib/utils/username'; +import { leafClient } from '$lib/leaf'; +import { CommonMark, Name } from 'leaf-proto/components'; +import { Page } from '../../../types'; + +export const load: PageServerLoad = async ({ + params +}): Promise<{ page: Page; revisionAuthor: string }> => { + const username = parseUsername(params.username); + if (username.domain == env.PUBLIC_DOMAIN) { + return redirect(302, `/${username.name}/${params.slug}`); + } + const fullUsername = `${username.name}@${username.domain || env.PUBLIC_DOMAIN}`; + const profileLink = await profileLinkByUsername(fullUsername); + + if (!profileLink) return error(404, `User not found: ${fullUsername}`); + const revisionLink = appendSubpath(profileLink, params.slug, { Uint: BigInt(params.revision!) }); + + const ent = await leafClient.get_components( + revisionLink, + CommonMark, + WebLinks, + Name, + WeirdWikiRevisionAuthor + ); + if (!ent) return error(404, 'Revision not found'); + + let display_name = ent.get(Name)?.value; + let links = ent.get(WebLinks)?.value; + + const commonMark = ent.get(CommonMark)?.value; + + if (!display_name) { + display_name = env.PUBLIC_INSTANCE_NAME; + } + + const authorId = ent.get(WeirdWikiRevisionAuthor)?.value; + const revisionAuthor = + (await (async () => { + if (!authorId) return; + const profileLink = profileLinkById(authorId); + const ent = await leafClient.get_components(profileLink, Username); + const username = ent?.get(Username)?.value; + return username; + })()) || ''; + + return { + page: { + slug: params.slug, + display_name, + markdown: commonMark || '', + links: links || [], + wiki: false + }, + revisionAuthor + }; +}; diff --git a/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.svelte b/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.svelte new file mode 100644 index 0000000..3db628b --- /dev/null +++ b/src/routes/(app)/[username]/[slug]/revisions/[revision]/+page.svelte @@ -0,0 +1,71 @@ + + + + + {data.page.display_name} | {data.profile.display_name} | + {env.PUBLIC_INSTANCE_NAME} + + + +
+

Revision Of

+ +

+ {$page.params.username} / {$page.params.slug} +

+ + {new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).format(dateFromUnixTimestamp(BigInt($page.params.revision)))} + +
+ by {data.revisionAuthor.split('@')[0]} +
+ +
+

+ {data.page.display_name} +

+ +
+ +
+
+ {@html renderMarkdownSanitized(data.page.markdown)} +
+ {#if data.page.links.length > 0} +
+

Links

+ +
+ {/if} +
+
+
diff --git a/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.server.ts b/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.server.ts index 58c3378..0663b1f 100644 --- a/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.server.ts +++ b/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.server.ts @@ -2,7 +2,15 @@ import { leafClient, type KnownComponents, loadKnownComponents } from '$lib/leaf import { base32Decode, base32Encode, type EntityPath, type ExactLink } from 'leaf-proto'; import type { Actions, PageServerLoad } from './$types'; import { error, fail, redirect } from '@sveltejs/kit'; -import { Tags, Username, WebLinks, WeirdCustomDomain, WeirdPubpageTheme } from '$lib/leaf/profile'; +import { + Tags, + Username, + WebLinks, + WeirdCustomDomain, + WeirdPubpageTheme, + WeirdWikiPage, + WeirdWikiRevisionAuthor +} from '$lib/leaf/profile'; import { CommonMark, Description, Name } from 'leaf-proto/components'; import { getSession } from '$lib/rauthy/server'; @@ -113,6 +121,18 @@ export const actions = { if (commonMark == '') commonMark = undefined; components.push(commonMark ? new CommonMark(commonMark) : CommonMark); + let weirdWikiPage: boolean = !!formData.get('weirdWikiPage') || false; + components.push(weirdWikiPage ? new WeirdWikiPage() : WeirdWikiPage); + + let weirdWikiRevisionAuthor: string | undefined = + formData.get('weirdWikiRevisionAuthor')?.toString() || ''; + if (weirdWikiRevisionAuthor == '') weirdWikiRevisionAuthor = undefined; + components.push( + weirdWikiRevisionAuthor + ? new WeirdWikiRevisionAuthor(weirdWikiRevisionAuthor) + : WeirdWikiRevisionAuthor + ); + await leafClient.update_components(link, components); } catch (e: any) { return fail(400, { error: JSON.stringify(e) }); diff --git a/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.svelte b/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.svelte index 8df077a..3eb9671 100644 --- a/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.svelte +++ b/src/routes/(internal)/__internal__/admin/explorer/[[namespace]]/[[subspace]]/[[entityPath]]/+page.svelte @@ -84,8 +84,13 @@