From 68972c25a3215f8a146fe40c004cbc98c461b20b Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sun, 19 Apr 2026 16:15:48 +0700 Subject: [PATCH 1/3] fix(communities): unwrap stats cid content --- src/hooks/communities.test.ts | 70 +++++++++++++++++++++++++++++++++++ src/hooks/communities.ts | 12 +++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/hooks/communities.test.ts b/src/hooks/communities.test.ts index ccb83af8..24aef407 100644 --- a/src/hooks/communities.test.ts +++ b/src/hooks/communities.test.ts @@ -532,6 +532,76 @@ describe("communities", () => { expect(rendered.result.current.hourActiveUserCount).toBe(1); }); + test("useCommunityStats unwraps fetchCid content responses", async () => { + const origFetch = PKC.prototype.fetchCid; + (PKC.prototype as any).fetchCid = () => + Promise.resolve({ + content: { + hourActiveUserCount: 3, + weekActiveUserCount: 33, + allPostCount: 333, + }, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + try { + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(3); + expect(rendered.result.current.weekActiveUserCount).toBe(33); + expect(rendered.result.current.allPostCount).toBe(333); + } finally { + (PKC.prototype as any).fetchCid = origFetch; + } + }); + + test("useCommunityStats parses string fetchCid content responses", async () => { + const origFetch = PKC.prototype.fetchCid; + (PKC.prototype as any).fetchCid = () => + Promise.resolve({ + content: JSON.stringify({ + hourActiveUserCount: 4, + weekActiveUserCount: 44, + allPostCount: 444, + }), + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "string wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + try { + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(4); + expect(rendered.result.current.weekActiveUserCount).toBe(44); + expect(rendered.result.current.allPostCount).toBe(444); + } finally { + (PKC.prototype as any).fetchCid = origFetch; + } + }); + + test("useCommunityStats accepts object fetchCid responses", async () => { + const origFetch = PKC.prototype.fetchCid; + (PKC.prototype as any).fetchCid = () => + Promise.resolve({ + hourActiveUserCount: 5, + weekActiveUserCount: 55, + allPostCount: 555, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "object stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + try { + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(5); + expect(rendered.result.current.weekActiveUserCount).toBe(55); + expect(rendered.result.current.allPostCount).toBe(555); + } finally { + (PKC.prototype as any).fetchCid = origFetch; + } + }); + test("useCommunityStats fetchCid error logs (stmt 110)", async () => { const origFetch = PKC.prototype.fetchCid; (PKC.prototype as any).fetchCid = () => Promise.reject(new Error("fetchCid failed")); diff --git a/src/hooks/communities.ts b/src/hooks/communities.ts index 30fcb721..c5258ee8 100644 --- a/src/hooks/communities.ts +++ b/src/hooks/communities.ts @@ -26,6 +26,16 @@ import shallow from "zustand/shallow"; import { getChainProviders, getPkcCommunityAddresses } from "../lib/pkc-compat"; import { getCommunityRefKey, getUniqueSortedCommunityRefs } from "../lib/community-ref"; +const parseMaybeJson = (value: unknown) => (typeof value === "string" ? JSON.parse(value) : value); + +const parseFetchedCommunityStats = (fetchedCid: unknown): CommunityStats => { + const parsedCid = parseMaybeJson(fetchedCid); + if (parsedCid && typeof parsedCid === "object" && "content" in parsedCid) { + return parseMaybeJson((parsedCid as { content: unknown }).content) as CommunityStats; + } + return parsedCid as CommunityStats; +}; + /** * @param community - The community identifier, e.g. {name: 'memes.eth'} or {publicKey: '12D3KooW...'} * @param acountName - The nickname of the account, e.g. 'Account 1'. If no accountName is provided, use @@ -183,7 +193,7 @@ export function useCommunityStats(options?: UseCommunityStatsOptions): UseCommun let fetchedCid; try { fetchedCid = await account.pkc.fetchCid({ cid: communityStatsCid }); - fetchedCid = JSON.parse(fetchedCid); + fetchedCid = parseFetchedCommunityStats(fetchedCid); if (cancelled) { return; } From 80ac8a3fe0e0459b3b6fab2b8290f4dd17600813 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sun, 19 Apr 2026 22:28:45 +0700 Subject: [PATCH 2/3] fix(communities): address stats parser review --- src/hooks/communities.test.ts | 97 +++++++++++++++++++++++++---------- src/hooks/communities.ts | 17 +++++- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/src/hooks/communities.test.ts b/src/hooks/communities.test.ts index 24aef407..1a738022 100644 --- a/src/hooks/communities.test.ts +++ b/src/hooks/communities.test.ts @@ -533,72 +533,115 @@ describe("communities", () => { }); test("useCommunityStats unwraps fetchCid content responses", async () => { - const origFetch = PKC.prototype.fetchCid; - (PKC.prototype as any).fetchCid = () => - Promise.resolve({ + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ content: { hourActiveUserCount: 3, weekActiveUserCount: 33, allPostCount: 333, }, }); - const rendered = renderHook(() => - useCommunityStats({ community: { name: "wrapped stats" } }), - ); - const waitFor = testUtils.createWaitFor(rendered); - try { + const rendered = renderHook(() => + useCommunityStats({ community: { name: "wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); await waitFor(() => rendered.result.current.state === "succeeded"); expect(rendered.result.current.hourActiveUserCount).toBe(3); expect(rendered.result.current.weekActiveUserCount).toBe(33); expect(rendered.result.current.allPostCount).toBe(333); } finally { - (PKC.prototype as any).fetchCid = origFetch; + fetchSpy?.mockRestore(); } }); test("useCommunityStats parses string fetchCid content responses", async () => { - const origFetch = PKC.prototype.fetchCid; - (PKC.prototype as any).fetchCid = () => - Promise.resolve({ + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ content: JSON.stringify({ hourActiveUserCount: 4, weekActiveUserCount: 44, allPostCount: 444, }), }); - const rendered = renderHook(() => - useCommunityStats({ community: { name: "string wrapped stats" } }), - ); - const waitFor = testUtils.createWaitFor(rendered); - try { + const rendered = renderHook(() => + useCommunityStats({ community: { name: "string wrapped stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); await waitFor(() => rendered.result.current.state === "succeeded"); expect(rendered.result.current.hourActiveUserCount).toBe(4); expect(rendered.result.current.weekActiveUserCount).toBe(44); expect(rendered.result.current.allPostCount).toBe(444); } finally { - (PKC.prototype as any).fetchCid = origFetch; + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats parses string fetchCid responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue( + JSON.stringify({ + hourActiveUserCount: 5, + weekActiveUserCount: 55, + allPostCount: 555, + }), + ); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "string stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(5); + expect(rendered.result.current.weekActiveUserCount).toBe(55); + expect(rendered.result.current.allPostCount).toBe(555); + } finally { + fetchSpy?.mockRestore(); } }); test("useCommunityStats accepts object fetchCid responses", async () => { - const origFetch = PKC.prototype.fetchCid; - (PKC.prototype as any).fetchCid = () => - Promise.resolve({ + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + hourActiveUserCount: 6, + weekActiveUserCount: 66, + allPostCount: 666, + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "object stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.hourActiveUserCount).toBe(6); + expect(rendered.result.current.weekActiveUserCount).toBe(66); + expect(rendered.result.current.allPostCount).toBe(666); + } finally { + fetchSpy?.mockRestore(); + } + }); + + test("useCommunityStats keeps direct stats responses with content fields", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: "not json stats content", hourActiveUserCount: 5, weekActiveUserCount: 55, allPostCount: 555, }); - const rendered = renderHook(() => - useCommunityStats({ community: { name: "object stats" } }), - ); - const waitFor = testUtils.createWaitFor(rendered); - try { + const rendered = renderHook(() => + useCommunityStats({ community: { name: "stats with content" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); await waitFor(() => rendered.result.current.state === "succeeded"); expect(rendered.result.current.hourActiveUserCount).toBe(5); expect(rendered.result.current.weekActiveUserCount).toBe(55); expect(rendered.result.current.allPostCount).toBe(555); + expect(rendered.result.current.content).toBe("not json stats content"); } finally { - (PKC.prototype as any).fetchCid = origFetch; + fetchSpy?.mockRestore(); } }); diff --git a/src/hooks/communities.ts b/src/hooks/communities.ts index c5258ee8..0b676e4f 100644 --- a/src/hooks/communities.ts +++ b/src/hooks/communities.ts @@ -28,10 +28,23 @@ import { getCommunityRefKey, getUniqueSortedCommunityRefs } from "../lib/communi const parseMaybeJson = (value: unknown) => (typeof value === "string" ? JSON.parse(value) : value); +const isRecord = (value: unknown): value is Record => + !!value && typeof value === "object"; + +const isCommunityStatsPayload = (value: unknown): value is CommunityStats => + isRecord(value) && + ("hourActiveUserCount" in value || "weekActiveUserCount" in value || "allPostCount" in value); + const parseFetchedCommunityStats = (fetchedCid: unknown): CommunityStats => { const parsedCid = parseMaybeJson(fetchedCid); - if (parsedCid && typeof parsedCid === "object" && "content" in parsedCid) { - return parseMaybeJson((parsedCid as { content: unknown }).content) as CommunityStats; + if (isCommunityStatsPayload(parsedCid)) { + return parsedCid; + } + if (isRecord(parsedCid) && "content" in parsedCid) { + const parsedContent = parseMaybeJson(parsedCid.content); + if (isCommunityStatsPayload(parsedContent)) { + return parsedContent; + } } return parsedCid as CommunityStats; }; From 0bf5cbb2150773c7cb60451f10d2dbecc90b0e79 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Mon, 20 Apr 2026 04:47:44 +0700 Subject: [PATCH 3/3] fix(communities): preserve unknown stats content --- src/hooks/communities.test.ts | 17 +++++++++++++++++ src/hooks/communities.ts | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/hooks/communities.test.ts b/src/hooks/communities.test.ts index 1a738022..9f4c4cc6 100644 --- a/src/hooks/communities.test.ts +++ b/src/hooks/communities.test.ts @@ -645,6 +645,23 @@ describe("communities", () => { } }); + test("useCommunityStats keeps unrecognized content responses", async () => { + let fetchSpy: { mockRestore: () => void } | undefined; + try { + fetchSpy = vi.spyOn(PKC.prototype as any, "fetchCid").mockResolvedValue({ + content: "not json stats content", + }); + const rendered = renderHook(() => + useCommunityStats({ community: { name: "unknown content stats" } }), + ); + const waitFor = testUtils.createWaitFor(rendered); + await waitFor(() => rendered.result.current.state === "succeeded"); + expect(rendered.result.current.content).toBe("not json stats content"); + } finally { + fetchSpy?.mockRestore(); + } + }); + test("useCommunityStats fetchCid error logs (stmt 110)", async () => { const origFetch = PKC.prototype.fetchCid; (PKC.prototype as any).fetchCid = () => Promise.reject(new Error("fetchCid failed")); diff --git a/src/hooks/communities.ts b/src/hooks/communities.ts index 0b676e4f..217b617e 100644 --- a/src/hooks/communities.ts +++ b/src/hooks/communities.ts @@ -28,6 +28,14 @@ import { getCommunityRefKey, getUniqueSortedCommunityRefs } from "../lib/communi const parseMaybeJson = (value: unknown) => (typeof value === "string" ? JSON.parse(value) : value); +const tryParseMaybeJson = (value: unknown) => { + try { + return parseMaybeJson(value); + } catch { + return value; + } +}; + const isRecord = (value: unknown): value is Record => !!value && typeof value === "object"; @@ -41,7 +49,7 @@ const parseFetchedCommunityStats = (fetchedCid: unknown): CommunityStats => { return parsedCid; } if (isRecord(parsedCid) && "content" in parsedCid) { - const parsedContent = parseMaybeJson(parsedCid.content); + const parsedContent = tryParseMaybeJson(parsedCid.content); if (isCommunityStatsPayload(parsedContent)) { return parsedContent; }