mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(admin): native-feed detail group + dismissable notice
Wire the NativeFeeds chip group into the per-feed emails page, add a dismissable banner that nudges users to subscribe directly, the dismiss POST route mirroring the confirmation-dismiss idiom, and the client-side handler in emails-page.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1603,5 +1603,74 @@ describe("Admin Routes", () => {
|
||||
const cfg = await repo.getConfig(feedId);
|
||||
expect(cfg?.sender_in_title).toBe(false);
|
||||
});
|
||||
|
||||
it("feed detail shows a native-feeds group when a native feed was detected", async () => {
|
||||
const authCookie = await loginAndGetCookie();
|
||||
const repo = FeedRepository.from(mockEnv as unknown as Env);
|
||||
const feedId = FeedId.generate();
|
||||
const mailboxId = MailboxId.unchecked("native.detail.07");
|
||||
const feed = Feed.create(
|
||||
feedId,
|
||||
{ title: "N", language: "en", allowedSenders: [], blockedSenders: [] },
|
||||
{ mailboxId },
|
||||
);
|
||||
feed.ingest(
|
||||
{ key: "k1", subject: "s", receivedAt: 1, size: 10 },
|
||||
{
|
||||
maxBytes: 1e9,
|
||||
nativeFeeds: {
|
||||
senderKey: "a@x.com",
|
||||
feeds: [{ url: "https://blog.example.com/feed.xml", type: "rss" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
await repo.save(feed);
|
||||
|
||||
const res = await request(`/admin/feeds/${feedId.value}/emails`, {
|
||||
headers: { Cookie: authCookie },
|
||||
});
|
||||
const body = await res.text();
|
||||
expect(body).toContain("native-feeds");
|
||||
expect(body).toContain("https://blog.example.com/feed.xml");
|
||||
});
|
||||
|
||||
it("native-feed dismiss route clears the flag", async () => {
|
||||
const authCookie = await loginAndGetCookie();
|
||||
const repo = FeedRepository.from(mockEnv as unknown as Env);
|
||||
const feedId = FeedId.generate();
|
||||
const mailboxId = MailboxId.unchecked("native.dismiss.09");
|
||||
const feed = Feed.create(
|
||||
feedId,
|
||||
{ title: "N", language: "en", allowedSenders: [], blockedSenders: [] },
|
||||
{ mailboxId },
|
||||
);
|
||||
feed.ingest(
|
||||
{ key: "k1", subject: "s", receivedAt: 1, size: 10 },
|
||||
{
|
||||
maxBytes: 1e9,
|
||||
nativeFeeds: {
|
||||
senderKey: "a@x.com",
|
||||
feeds: [{ url: "https://x.com/rss", type: "rss" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
await repo.save(feed);
|
||||
|
||||
const res = await request(
|
||||
`/admin/feeds/${feedId.value}/native-feed/dismiss`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: authCookie,
|
||||
"Content-Type": "application/json",
|
||||
Origin: `https://${mockEnv.DOMAIN}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const reloaded = await repo.load(feedId);
|
||||
expect(reloaded!.hasNativeFeed()).toBe(false);
|
||||
expect(reloaded!.nativeFeeds()).toHaveLength(1); // URLs preserved
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
CheckIcon,
|
||||
FeedFormats,
|
||||
ExpiryBadge,
|
||||
NativeFeeds,
|
||||
} from "./ui";
|
||||
import { unionNativeFeeds } from "../../domain/native-feed";
|
||||
import {
|
||||
deleteAttachmentsForEmails,
|
||||
deleteKeysWithConcurrency,
|
||||
@@ -140,6 +142,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
|
||||
return c.text("Feed not found", 404);
|
||||
}
|
||||
|
||||
const nativeFeeds = unionNativeFeeds(feedMetadata.nativeFeeds);
|
||||
const emailAddress = feedEmailAddress(feedConfig.mailbox_id, env);
|
||||
|
||||
return c.html(
|
||||
@@ -164,6 +167,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
|
||||
)}
|
||||
<CopyField label="Email:" value={emailAddress} />
|
||||
<FeedFormats feedId={feedId} env={env} />
|
||||
<NativeFeeds feeds={nativeFeeds} />
|
||||
</div>
|
||||
|
||||
{feedMetadata.pendingConfirmation && (
|
||||
@@ -185,6 +189,28 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nativeFeeds.length > 0 && !feedMetadata.nativeFeedDismissed && (
|
||||
<div
|
||||
class="confirmation-banner"
|
||||
id="native-feed-banner"
|
||||
data-feed-id={feedId}
|
||||
>
|
||||
<span>
|
||||
This newsletter publishes its own feed — subscribe to it directly
|
||||
from "Native feeds" above.
|
||||
</span>
|
||||
<div class="confirmation-banner-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small"
|
||||
id="native-feed-dismiss"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2>
|
||||
Emails (
|
||||
<span id="email-total-count">{feedMetadata.emails.length}</span>)
|
||||
@@ -733,6 +759,32 @@ emailsRouter.post("/feeds/:feedId/confirmation/dismiss", async (c) => {
|
||||
: c.redirect(`/admin/feeds/${feedId}/emails`);
|
||||
});
|
||||
|
||||
// ── Dismiss native-feed notice ───────────────────────────────────────────────
|
||||
|
||||
emailsRouter.post("/feeds/:feedId/native-feed/dismiss", async (c) => {
|
||||
const env = c.env;
|
||||
const repo = FeedRepository.from(env);
|
||||
const feedId = c.req.param("feedId");
|
||||
const wantsJson = (
|
||||
c.req.header("Accept") ||
|
||||
c.req.header("Content-Type") ||
|
||||
""
|
||||
).includes("application/json");
|
||||
|
||||
const feed = await repo.load(FeedId.unchecked(feedId));
|
||||
if (!feed) {
|
||||
return wantsJson
|
||||
? c.json({ ok: false, error: "Feed not found" }, 404)
|
||||
: c.text("Feed not found", 404);
|
||||
}
|
||||
feed.dismissNativeFeed();
|
||||
await repo.saveMetadata(feed);
|
||||
|
||||
return wantsJson
|
||||
? c.json({ ok: true })
|
||||
: c.redirect(`/admin/feeds/${feedId}/emails`);
|
||||
});
|
||||
|
||||
// ── Bulk delete emails ────────────────────────────────────────────────────────
|
||||
|
||||
emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
|
||||
@@ -634,3 +634,22 @@ if (dismissBtn && banner) {
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Native-feed banner dismiss ────────────────────────────────────────────────
|
||||
|
||||
const nativeDismissBtn = document.getElementById("native-feed-dismiss");
|
||||
const nativeBanner = document.getElementById("native-feed-banner");
|
||||
if (nativeDismissBtn && nativeBanner) {
|
||||
nativeDismissBtn.addEventListener("click", () => {
|
||||
const feedId = nativeBanner.getAttribute("data-feed-id") ?? "";
|
||||
fetch(`/admin/feeds/${feedId}/native-feed/dismiss`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if ((d as { ok?: boolean }).ok) nativeBanner.remove();
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user