This commit is contained in:
Râu Cao 2024-10-28 15:01:17 +01:00
parent cea96e170d
commit 1e081c83e5
Signed by: raucao
GPG Key ID: 37036C356E56CC51
9 changed files with 101 additions and 5 deletions

View File

@ -142,6 +142,14 @@ main article footer {
margin-top: 5rem; margin-top: 5rem;
} }
main article footer .actions {
margin-bottom: 3rem;
}
main article footer p {
font-size: 1rem;
}
.nip05 .verified, .nip05 .verified,
.nip05 .not-verified { .nip05 .not-verified {
margin-left: 0.3rem; margin-left: 0.3rem;

View File

@ -76,6 +76,17 @@ main.profile-page .pubkey {
color: var(--text-color-discreet); color: var(--text-color-discreet);
} }
main article footer a,
main article footer a:visited {
text-decoration: none;
color: var(--text-color-discreet);
}
main article footer a:hover,
main article footer a:active {
text-decoration: underline;
}
.nip05 .verified { .nip05 .verified {
background-color: #e8e3da; background-color: #e8e3da;
color: #027739; color: #027739;

View File

@ -19,7 +19,9 @@ const userProfileHandler = async function (ctx: Context) {
if (profileEvent) { if (profileEvent) {
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
const articles = await fetchArticlesByAuthor(pubkey, 210);
const articles = await fetchArticlesByAuthor(pubkey, 210, ctx.state.tags);
const html = await profilePageHtml(profile, articles); const html = await profilePageHtml(profile, articles);
generateOgProfileImage(profile); generateOgProfileImage(profile);

17
html.ts
View File

@ -38,6 +38,16 @@ export function errorPageHtml(statusCode: number, title: string): string {
return htmlLayout({ title, body }); return htmlLayout({ title, body });
} }
function articleTagsHtml(article: Article, profile: Profile): string {
if (article.tags.length === 0) return "";
const tags = article.tags.map((tag) => {
return `<a href="/@${profile.username}?tags=${tag}">${tag}</a>`;
});
return `Tags: ${tags.join(", ")}</p>\n`;
}
export async function articleHtml( export async function articleHtml(
article: Article, article: Article,
profile: Profile, profile: Profile,
@ -65,7 +75,12 @@ export async function articleHtml(
<article> <article>
${await article.buildContentHtml()} ${await article.buildContentHtml()}
<footer> <footer>
${openWithNostrAppHtml(article.naddr)} <div class="actions">
${openWithNostrAppHtml(article.naddr)}
</div>
<p class="tags">
${articleTagsHtml(article, profile)}
</p>
</footer> </footer>
</article> </article>
</main> </main>

View File

@ -39,6 +39,13 @@ export default class Article {
return tag ? tag[1] : ""; return tag ? tag[1] : "";
} }
get tags(): string[] {
return this.event.tags
.filter((t) => t[0] === "t")
.filter((t) => t[1] !== "")
.map((t) => t[1]);
}
get publishedAt(): number { get publishedAt(): number {
const tag = this.event.tags.find((t) => t[0] === "published_at"); const tag = this.event.tags.find((t) => t[0] === "published_at");
return tag ? parseInt(tag[1]) : this.event.created_at; return tag ? parseInt(tag[1]) : this.event.created_at;

View File

@ -15,7 +15,7 @@ const relayPool = new NPool({
eventRouter: async (_event) => [], eventRouter: async (_event) => [],
}); });
async function fetchWithTimeout(filters: NostrFilter[]) { export async function fetchWithTimeout(filters: NostrFilter[]) {
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("relay timeout")), config.query_timeout) setTimeout(() => reject(new Error("relay timeout")), config.query_timeout)
); );
@ -50,16 +50,28 @@ export async function fetchReplaceableEvent(
} }
} }
export function createTagList(
articles: Article[],
): Record<string, number> {
return articles.flatMap((a) => a.tags).reduce((acc, tag) => {
acc[tag] = (acc[tag] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
export async function fetchArticlesByAuthor( export async function fetchArticlesByAuthor(
pubkey: string, pubkey: string,
limit: number = 10, limit: number = 10,
tags?: string[],
) { ) {
const events = await fetchWithTimeout([{ const filter = {
authors: [pubkey], authors: [pubkey],
kinds: [30023], kinds: [30023],
limit: limit, limit: limit,
}]) as NostrEvent[]; };
if (typeof tags !== "undefined") filter["#t"] = tags;
const events = await fetchWithTimeout([filter]) as NostrEvent[];
const articles = events.map((a) => new Article(a)); const articles = events.map((a) => new Article(a));
return articles return articles

View File

@ -26,6 +26,10 @@ router.get("/:path", async (ctx) => {
} else if (path.startsWith("npub")) { } else if (path.startsWith("npub")) {
await npubHandler(ctx); await npubHandler(ctx);
} else if (path.startsWith("@") || path.startsWith("~")) { } else if (path.startsWith("@") || path.startsWith("~")) {
const tags = ctx.request.url.searchParams.get("tags");
if (typeof tags === "string" && tags !== "") {
ctx.state.tags = tags.split(",");
}
await userProfileHandler(ctx); await userProfileHandler(ctx);
} else { } else {
notFoundHandler(ctx); notFoundHandler(ctx);

View File

@ -47,6 +47,16 @@ describe("Article", () => {
}); });
}); });
describe("#tags", () => {
it("returns a flattened tag list", () => {
expect(article.tags).toEqual([
"lightning",
"lightning network",
"howto",
]);
});
});
describe("#publishedAt", () => { describe("#publishedAt", () => {
it("returns the value of the first 'published_at' tag", () => { it("returns the value of the first 'published_at' tag", () => {
expect(article.publishedAt).toEqual(1726402055); expect(article.publishedAt).toEqual(1726402055);

27
tests/nostr_test.ts Normal file
View File

@ -0,0 +1,27 @@
// import { describe, it } from "@std/testing/bdd";
// import { stub } from "@std/testing/mock";
// import { expect } from "@std/expect";
// import { NostrEvent, NostrFilter } from "@nostrify/nostrify";
// import * as nostr from "../nostr.ts";
//
// async function fetchWithTimeout(filters: NostrFilter[]) {
// console.log("================")
// const events = [];
// const fixtures = [ "article-1.json", "article-deleted.json" ]
// for (const filename of fixtures) {
// const event = JSON.parse(Deno.readTextFileSync(`tests/fixtures/${filename}`));
// events.push(event);
// }
// return Promise.resolve(events);
// }
//
// describe("Nostr", () => {
// describe("#fetchArticlesByAuthor", () => {
// it("removes the anchor links for headlines", async () => {
// stub(nostr, "fetchArticlesByAuthor");
//
// const articles = await nostr.fetchArticlesByAuthor("123456abcdef");
// expect(articles.length).toEqual(2);
// });
// });
// });