Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,27 @@ export async function apiVerify(input: {
);
}

export async function apiFeed(input?: {
export type FeedQueryInput = {
stage?: string;
excludeStages?: string[];
excludeEntityTypes?: string[];
cursor?: string;
actor?: string;
chambers?: string[];
limit?: number;
}): Promise<GetFeedResponse> {
};

export async function apiFeed(
input?: FeedQueryInput,
): Promise<GetFeedResponse> {
const params = new URLSearchParams();
if (input?.stage) params.set("stage", input.stage);
if (input?.excludeStages && input.excludeStages.length > 0) {
params.set("excludeStages", input.excludeStages.join(","));
}
if (input?.excludeEntityTypes && input.excludeEntityTypes.length > 0) {
params.set("excludeEntityTypes", input.excludeEntityTypes.join(","));
}
if (input?.cursor) params.set("cursor", input.cursor);
if (input?.actor) params.set("actor", input.actor);
if (input?.chambers && input.chambers.length > 0) {
Expand Down
110 changes: 110 additions & 0 deletions src/lib/feedScopeRouting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { FeedQueryInput } from "@/lib/apiClient";

export type FeedScope = "urgent" | "my" | "chambers" | "system" | "all";

export const FEED_SCOPES: { value: FeedScope; label: string }[] = [
{ value: "urgent", label: "Urgent" },
{ value: "my", label: "My activity" },
{ value: "chambers", label: "Chambers and factions" },
{ value: "system", label: "System" },
{ value: "all", label: "All activity" },
];

export const SYSTEM_FEED_STAGE = "system";
export const PRIVATE_FEED_ENTITY_TYPES = ["faction_invite"] as const;

const ACTIVITY_EXCLUDED_STAGES = [SYSTEM_FEED_STAGE];

export function feedScopeRequiresWallet(scope: FeedScope) {
return scope === "urgent" || scope === "my" || scope === "chambers";
}

export function feedScopeRequiresChambers(scope: FeedScope) {
return scope === "urgent" || scope === "chambers";
}

export function buildFeedRequestForScope(input: {
scope: Exclude<FeedScope, "urgent">;
address?: string;
chamberFilters?: string[] | null;
cursor?: string | null;
limit: number;
}): FeedQueryInput {
const base: FeedQueryInput = {
cursor: input.cursor ?? undefined,
limit: input.limit,
};

if (input.scope === "my") {
return {
...base,
actor: input.address,
excludeStages: ACTIVITY_EXCLUDED_STAGES,
};
}

if (input.scope === "chambers") {
return {
...base,
chambers: input.chamberFilters ?? [],
excludeStages: ACTIVITY_EXCLUDED_STAGES,
excludeEntityTypes: [...PRIVATE_FEED_ENTITY_TYPES],
};
}

if (input.scope === "system") {
return {
...base,
stage: SYSTEM_FEED_STAGE,
};
}

return {
...base,
excludeEntityTypes: [...PRIVATE_FEED_ENTITY_TYPES],
};
}

export function buildUrgentFeedRequests(input: {
address?: string;
chamberFilters: string[];
baseLimit: number;
stageLimit: number;
factionLimit: number;
}): FeedQueryInput[] {
const requests: FeedQueryInput[] = [
{
chambers: input.chamberFilters,
excludeStages: ACTIVITY_EXCLUDED_STAGES,
excludeEntityTypes: [...PRIVATE_FEED_ENTITY_TYPES],
limit: input.baseLimit,
},
{
stage: "pool",
chambers: input.chamberFilters,
limit: input.stageLimit,
},
{
stage: "vote",
chambers: input.chamberFilters,
limit: input.stageLimit,
},
];

if (input.address) {
requests.push(
{
stage: "build",
actor: input.address,
limit: input.stageLimit,
},
{
stage: "faction",
actor: input.address,
limit: input.factionLimit,
},
);
}

return requests;
}
14 changes: 14 additions & 0 deletions src/lib/feedUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,17 @@ export const toUrgentItems = (
}
return Array.from(deduped.values());
};

export const toLimitedUrgentItems = (
items: FeedItemDto[],
isGovernorActive: boolean,
viewerAddress: string | undefined,
limit: number,
): FeedItemDto[] => {
const safeLimit = Math.max(0, Math.floor(limit));
if (safeLimit === 0) return [];
return toUrgentItems(items, isGovernorActive, viewerAddress).slice(
0,
safeLimit,
);
};
3 changes: 2 additions & 1 deletion src/pages/feed/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useAuth } from "@/app/auth/AuthContext";
import { PageHint } from "@/components/PageHint";
import { factionIdFromHref, feedItemKey } from "@/lib/feedUi";
import type { FeedScope } from "@/lib/feedScopeRouting";
import {
apiFactionCofounderInviteAccept,
apiFactionCofounderInviteDecline,
} from "@/lib/apiClient";
import type { FeedItemDto } from "@/types/api";
import { FeedControls, type FeedScope } from "./components/FeedControls";
import { FeedControls } from "./components/FeedControls";
import { FeedListSection } from "./components/FeedListSection";
import { FeedStatusMessages } from "./components/FeedStatusMessages";
import { useFeedChamberFilters } from "./hooks/useFeedChamberFilters";
Expand Down
10 changes: 1 addition & 9 deletions src/pages/feed/components/FeedControls.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { ToggleGroup } from "@/components/ToggleGroup";

export type FeedScope = "urgent" | "my" | "chambers" | "all";

const FEED_SCOPES: { value: FeedScope; label: string }[] = [
{ value: "urgent", label: "Urgent" },
{ value: "my", label: "My activity" },
{ value: "chambers", label: "Chambers and factions" },
{ value: "all", label: "All activity" },
];
import { FEED_SCOPES, type FeedScope } from "@/lib/feedScopeRouting";

type FeedControlsProps = {
feedScope: FeedScope;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/feed/hooks/useFeedChamberFilters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";

import { apiClock, apiHuman, apiMyGovernance } from "@/lib/apiClient";
import type { FeedScope } from "../components/FeedControls";
import type { FeedScope } from "@/lib/feedScopeRouting";

export function useFeedChamberFilters(input: {
address?: string | null;
Expand Down
120 changes: 41 additions & 79 deletions src/pages/feed/hooks/useFeedItems.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useCallback, useEffect, useMemo, useState } from "react";

import { toTimestampMs } from "@/lib/dateTime";
import { feedItemKey, toUrgentItems } from "@/lib/feedUi";
import { feedItemKey, toLimitedUrgentItems } from "@/lib/feedUi";
import {
buildFeedRequestForScope,
buildUrgentFeedRequests,
feedScopeRequiresChambers,
feedScopeRequiresWallet,
} from "@/lib/feedScopeRouting";
import type { FeedScope } from "@/lib/feedScopeRouting";
import { apiFeed } from "@/lib/apiClient";
import type { FeedItemDto } from "@/types/api";
import type { FeedScope } from "../components/FeedControls";
import { FEED_MAX_PAGE_SIZE, FEED_MIN_PAGE_SIZE } from "./useFeedPageSize";

const URGENT_STAGE_LIMIT = FEED_MAX_PAGE_SIZE * 2;
Expand All @@ -15,52 +21,21 @@ async function loadUrgentFeedItems(input: {
limit: number;
isGovernorActive: boolean;
}): Promise<FeedItemDto[]> {
const [base, pool, vote, build, invites, system] = await Promise.all([
apiFeed({ chambers: input.chambers, limit: input.limit }),
apiFeed({
stage: "pool",
chambers: input.chambers,
limit: URGENT_STAGE_LIMIT,
}),
apiFeed({
stage: "vote",
chambers: input.chambers,
limit: URGENT_STAGE_LIMIT,
}),
input.address
? apiFeed({
stage: "build",
actor: input.address,
limit: URGENT_STAGE_LIMIT,
})
: Promise.resolve({ items: [] as FeedItemDto[] }),
input.address
? apiFeed({
actor: input.address,
stage: "faction",
limit: FEED_MIN_PAGE_SIZE,
})
: Promise.resolve({ items: [] as FeedItemDto[] }),
input.address
? apiFeed({
actor: input.address,
stage: "system",
limit: URGENT_STAGE_LIMIT,
})
: Promise.resolve({ items: [] as FeedItemDto[] }),
]);
const responses = await Promise.all(
buildUrgentFeedRequests({
address: input.address,
chamberFilters: input.chambers,
baseLimit: input.limit,
stageLimit: URGENT_STAGE_LIMIT,
factionLimit: FEED_MIN_PAGE_SIZE,
}).map((request) => apiFeed(request)),
);

return toUrgentItems(
[
...base.items,
...pool.items,
...vote.items,
...build.items,
...invites.items,
...system.items,
],
return toLimitedUrgentItems(
responses.flatMap((response) => response.items),
input.isGovernorActive,
input.address,
input.limit,
);
}

Expand Down Expand Up @@ -90,19 +65,15 @@ export function useFeedItems({
useEffect(() => {
let active = true;
const loadFeed = async () => {
if (feedScope !== "all" && !address) {
if (feedScopeRequiresWallet(feedScope) && !address) {
setFeedItems([]);
onLoadError("Connect a wallet to view your feed.");
setNextCursor(null);
return;
}
if (feedScopeRequiresChambers(feedScope) && chambersLoading) return;
if (
(feedScope === "chambers" || feedScope === "urgent") &&
chambersLoading
)
return;
if (
(feedScope === "chambers" || feedScope === "urgent") &&
feedScopeRequiresChambers(feedScope) &&
chamberFilters &&
chamberFilters.length === 0
) {
Expand All @@ -126,12 +97,14 @@ export function useFeedItems({
onLoadError(null);
return;
}
const res = await apiFeed({
actor: feedScope === "my" ? (address ?? undefined) : undefined,
chambers:
feedScope === "chambers" ? (chamberFilters ?? []) : undefined,
limit: pageSize,
});
const res = await apiFeed(
buildFeedRequestForScope({
scope: feedScope,
address: address ?? undefined,
chamberFilters,
limit: pageSize,
}),
);
if (!active) return;
setFeedItems(res.items);
setNextCursor(res.nextCursor ?? null);
Expand Down Expand Up @@ -167,27 +140,17 @@ export function useFeedItems({
if (!nextCursor || loadingMore) return;
setLoadingMore(true);
try {
const res = await apiFeed({
cursor: nextCursor,
actor: feedScope === "my" ? (address ?? undefined) : undefined,
chambers:
feedScope === "chambers" || feedScope === "urgent"
? (chamberFilters ?? [])
: undefined,
limit: pageSize,
});
const items =
feedScope === "urgent"
? toUrgentItems(res.items, viewerGovernorActive, address ?? undefined)
: res.items;
const res = await apiFeed(
buildFeedRequestForScope({
scope: feedScope === "urgent" ? "all" : feedScope,
address: address ?? undefined,
chamberFilters,
cursor: nextCursor,
limit: pageSize,
}),
);
const items = res.items;
setFeedItems((curr) => {
if (feedScope === "urgent") {
return toUrgentItems(
[...(curr ?? []), ...items],
viewerGovernorActive,
address ?? undefined,
);
}
const existing = new Set((curr ?? []).map(feedItemKey));
const nextItems = items.filter(
(item) => !existing.has(feedItemKey(item)),
Expand All @@ -209,7 +172,6 @@ export function useFeedItems({
nextCursor,
onLoadError,
pageSize,
viewerGovernorActive,
]);

const dismissItem = useCallback((key: string) => {
Expand Down
Loading
Loading