feat(tauri-app): Notifications-View mit Badge, Follower-Listen, DID-Navigation
Bindet die neuen AppView-Endpoints an: sechs IPC-Commands
(fetch_notifications, notification_count, mark_notifications_seen,
fetch_followers, fetch_following, fetch_thread) plus profile_get_by_did,
dazu die TS-Gegenstücke.
* NotificationsView: Liste mit Icon/Text je Art, Avatar, Vorschau des
subject_text, Cursor-Pagination; Klick auf eine Zeile mit Subject öffnet
den Thread. Beim Öffnen wird mit dem indexed_at der obersten Zeile als
Wasserzeichen quittiert.
* NavRail: Eintrag mit Unread-Badge, gepollt im vorhandenen 5s-Timer und
über den bestehenden Teardown-Pfad abgeräumt; der Poll pausiert, solange
die Liste offen ist.
* ProfileView: Follower- und Following-Zahlen sind jetzt Buttons und öffnen
die jeweilige Liste inline.
Navigiert wird über die DID, nicht über den Handle: für Actors, die weder
profiles noch posts kennen, liefert die AppView einen abgeschnittenen
Platzhalter im handle-Feld ("did:plc:abcd…"), und ein Klick darauf landete
in einem synthetischen Leerprofil. Die echte DID steht im DTO und wird jetzt
explizit durchgereicht — keine Heuristik auf das Platzhalter-Format.
Dabei aufgefallen und mitgefixt: ProfileView lud nur in onMount, obwohl die
Komponente bei Profil-zu-Profil-Navigation gemountet bleibt — Posts und
Zahlen des vorherigen Nutzers wären unter dem neuen Namen stehengeblieben.
Jetzt ein auf (did, handle) gekeyter Effekt.
Das Thread-Overlay hing im else-Zweig der leeren Timeline und wäre aus dem
Notifications-View unsichtbar gewesen; es ist jetzt ein Snippet, das beide
Views rendern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
bce4c7862f
commit
31880e1005
@@ -5,6 +5,10 @@
|
||||
//
|
||||
// Pattern: `let view = $state("home")` in the harness, then the
|
||||
// `on_select` callback updates it (same shape as App.svelte).
|
||||
//
|
||||
// The rail order is: home, notifications, compose, profile, search,
|
||||
// settings. The indices below follow that order — if you reorder the
|
||||
// `items` array in NavRail.svelte, update them here too.
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { mount, unmount, tick } from "svelte";
|
||||
@@ -23,16 +27,16 @@ afterEach(() => {
|
||||
target.remove();
|
||||
});
|
||||
|
||||
async function mountHarness() {
|
||||
app = mount(NavRailHarness, { target });
|
||||
async function mountHarness(props: { unread?: number } = {}) {
|
||||
app = mount(NavRailHarness, { target, props });
|
||||
await tick();
|
||||
}
|
||||
|
||||
describe("NavRail (callback-prop pattern)", () => {
|
||||
it("renders 5 buttons with home active", async () => {
|
||||
it("renders 6 buttons with home active", async () => {
|
||||
await mountHarness();
|
||||
const btns = target.querySelectorAll("button.rail__btn");
|
||||
expect(btns.length).toBe(5);
|
||||
expect(btns.length).toBe(6);
|
||||
expect(btns[0].classList.contains("active")).toBe(true);
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("home");
|
||||
});
|
||||
@@ -41,25 +45,68 @@ describe("NavRail (callback-prop pattern)", () => {
|
||||
await mountHarness();
|
||||
const btns = target.querySelectorAll("button.rail__btn");
|
||||
|
||||
btns[3].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
btns[4].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("search");
|
||||
expect(btns[3].classList.contains("active")).toBe(true);
|
||||
expect(btns[4].classList.contains("active")).toBe(true);
|
||||
expect(btns[0].classList.contains("active")).toBe(false);
|
||||
|
||||
btns[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("compose");
|
||||
expect(btns[1].classList.contains("active")).toBe(true);
|
||||
|
||||
btns[2].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("profile");
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("compose");
|
||||
expect(btns[2].classList.contains("active")).toBe(true);
|
||||
|
||||
btns[4].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
btns[3].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("profile");
|
||||
expect(btns[3].classList.contains("active")).toBe(true);
|
||||
|
||||
btns[5].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe("settings");
|
||||
expect(btns[4].classList.contains("active")).toBe(true);
|
||||
expect(btns[5].classList.contains("active")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("routes to the notifications view", async () => {
|
||||
await mountHarness();
|
||||
const btns = target.querySelectorAll("button.rail__btn");
|
||||
|
||||
btns[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
expect(target.querySelector('[data-testid="view-value"]')?.textContent).toBe(
|
||||
"notifications",
|
||||
);
|
||||
expect(btns[1].classList.contains("active")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NavRail unread badge", () => {
|
||||
it("renders no badge at zero unread", async () => {
|
||||
await mountHarness({ unread: 0 });
|
||||
expect(target.querySelector('[data-testid="notif-badge"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the count on the notifications button", async () => {
|
||||
await mountHarness({ unread: 3 });
|
||||
const badge = target.querySelector('[data-testid="notif-badge"]');
|
||||
expect(badge).not.toBeNull();
|
||||
expect(badge?.textContent?.trim()).toBe("3");
|
||||
// The badge must sit on the notifications entry, not on some
|
||||
// other rail button.
|
||||
const btns = target.querySelectorAll("button.rail__btn");
|
||||
expect(btns[1].contains(badge!)).toBe(true);
|
||||
});
|
||||
|
||||
it("caps a large backlog at 99+ so the 88px rail can't widen", async () => {
|
||||
await mountHarness({ unread: 1234 });
|
||||
const badge = target.querySelector('[data-testid="notif-badge"]');
|
||||
expect(badge?.textContent?.trim()).toBe("99+");
|
||||
});
|
||||
|
||||
it("keeps 99 unabbreviated (the cap is exclusive)", async () => {
|
||||
await mountHarness({ unread: 99 });
|
||||
expect(
|
||||
target.querySelector('[data-testid="notif-badge"]')?.textContent?.trim(),
|
||||
).toBe("99");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user