WIP Tags
This commit is contained in:
parent
cea96e170d
commit
1e081c83e5
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
15
html.ts
15
html.ts
@ -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>
|
||||||
|
<div class="actions">
|
||||||
${openWithNostrAppHtml(article.naddr)}
|
${openWithNostrAppHtml(article.naddr)}
|
||||||
|
</div>
|
||||||
|
<p class="tags">
|
||||||
|
${articleTagsHtml(article, profile)}
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
@ -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;
|
||||||
|
18
nostr.ts
18
nostr.ts
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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
27
tests/nostr_test.ts
Normal 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);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
Loading…
x
Reference in New Issue
Block a user