Compare commits
2 Commits
20d717f879
...
dde3fe8fe2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde3fe8fe2 | ||
|
|
6026286bbf |
@@ -12,6 +12,7 @@ import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
|
||||
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
|
||||
import { useAuth } from "./hooks/useAuth.ts";
|
||||
import { useDefaultFeedTab } from "./hooks/useDefaultFeedTab.ts";
|
||||
import { DEFAULT_FEED_TAB } from "./config/feedTabs.ts";
|
||||
|
||||
import { AuthProvider } from "./contexts/AuthProvider.tsx";
|
||||
import { PlayerProvider } from "./contexts/PlayerProvider.tsx";
|
||||
@@ -83,7 +84,7 @@ const NotFound = lazy(() =>
|
||||
function useResolvedDefaultTab() {
|
||||
const { user } = useAuth();
|
||||
const [preferredTab] = useDefaultFeedTab();
|
||||
return preferredTab === "followed" && !user ? "hot" : preferredTab;
|
||||
return preferredTab === "followed" && !user ? DEFAULT_FEED_TAB : preferredTab;
|
||||
}
|
||||
|
||||
// The query string has to survive the hop: the Web Share Target posts to "/",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createPortal } from "react-dom";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useCategories } from "../hooks/useCategories.ts";
|
||||
import { DEFAULT_FEED_TAB } from "../config/feedTabs.ts";
|
||||
|
||||
/**
|
||||
* Header dropdown that switches the feed scope between "All" and a category,
|
||||
@@ -73,7 +74,7 @@ export function CategorySwitcher() {
|
||||
if (categories.length === 0) return null;
|
||||
|
||||
const value = categorySlug ?? "~";
|
||||
const tab = feedTab ?? "hot";
|
||||
const tab = feedTab ?? DEFAULT_FEED_TAB;
|
||||
const currentLabel = categorySlug
|
||||
? categories.find((c) => c.slug === categorySlug)?.name ?? categorySlug
|
||||
: t`All`;
|
||||
|
||||
@@ -527,12 +527,12 @@ export function ChatModal({ onClose }: { onClose: () => void }) {
|
||||
return out;
|
||||
}, [messages]);
|
||||
|
||||
const typingLabel = useMemo(() => {
|
||||
const names = typingOthers.map((u) => u.username);
|
||||
if (names.length === 1) return t`${names[0]} is typing…`;
|
||||
if (names.length === 2) return t`${names[0]} and ${names[1]} are typing…`;
|
||||
return t`Several people are typing…`;
|
||||
}, [typingOthers]);
|
||||
const typingNames = typingOthers.map((u) => u.username);
|
||||
const typingLabel = typingNames.length === 1
|
||||
? t`${typingNames[0]} is typing…`
|
||||
: typingNames.length === 2
|
||||
? t`${typingNames[0]} and ${typingNames[1]} are typing…`
|
||||
: t`Several people are typing…`;
|
||||
|
||||
return (
|
||||
<Modal title={t`Chat`} onClose={onClose} wide>
|
||||
|
||||
@@ -333,7 +333,8 @@ export function DumpCreateModal(
|
||||
const toggleCategory = (id: string) =>
|
||||
setSelectedCategoryIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
import { DEFAULT_FEED_TAB, FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts";
|
||||
import { TabBar } from "./TabBar.tsx";
|
||||
import { CategorySwitcher } from "./CategorySwitcher.tsx";
|
||||
@@ -18,7 +18,7 @@ export function FeedTabBar() {
|
||||
|
||||
const [preferredTab] = useDefaultFeedTab();
|
||||
const defaultTab: FeedTab = preferredTab === "followed" && !user
|
||||
? "hot"
|
||||
? DEFAULT_FEED_TAB
|
||||
: preferredTab;
|
||||
const active: FeedTab = (FEED_TABS as readonly string[]).includes(feedTab ?? "")
|
||||
? (feedTab as FeedTab)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { type ComponentProps, useState } from "react";
|
||||
import { canProxyImage, proxyImageUrl } from "../utils/proxyImage.ts";
|
||||
import ThumbnailPlaceholder from "./ThumbnailPlaceholder.tsx";
|
||||
|
||||
@@ -9,12 +9,7 @@ interface ThumbnailProps {
|
||||
className?: string;
|
||||
loading?: "lazy" | "eager";
|
||||
/** Passed through to the placeholder when there's nothing to show. */
|
||||
placeholder: {
|
||||
url?: string;
|
||||
accentColor?: string;
|
||||
faviconUrl?: string;
|
||||
siteName?: string;
|
||||
};
|
||||
placeholder: Omit<ComponentProps<typeof ThumbnailPlaceholder>, "className">;
|
||||
/** Extra class for the placeholder element only. */
|
||||
placeholderClassName?: string;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const FEED_TABS = ["hot", "new", "journal", "followed"] as const;
|
||||
export type FeedTab = (typeof FEED_TABS)[number];
|
||||
|
||||
// Tab the index opens on for visitors and for users who haven't picked one.
|
||||
export const DEFAULT_FEED_TAB: FeedTab = "journal";
|
||||
|
||||
@@ -10,22 +10,30 @@ import {
|
||||
type RawCategory,
|
||||
} from "../model.ts";
|
||||
|
||||
async function fetchCategories(
|
||||
signal?: AbortSignal,
|
||||
): Promise<Category[] | null> {
|
||||
const res = await fetch(`${API_URL}/api/categories`, { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
if (!body.success) return null;
|
||||
return (body.data as RawCategory[]).map(deserializeCategory);
|
||||
}
|
||||
|
||||
export function CategoriesProvider({ children }: { children: ReactNode }) {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const load = useCallback(async (signal?: AbortSignal) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/categories`, { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
if (!body.success) return;
|
||||
setCategories((body.data as RawCategory[]).map(deserializeCategory));
|
||||
setIsLoaded(true);
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== "AbortError") setIsLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
const load = useCallback((signal?: AbortSignal) =>
|
||||
fetchCategories(signal)
|
||||
.then((data) => {
|
||||
if (!data) return;
|
||||
setCategories(data);
|
||||
setIsLoaded(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).name !== "AbortError") setIsLoaded(true);
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
import { DEFAULT_FEED_TAB, FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
|
||||
const STORAGE_KEY = "default-feed-tab";
|
||||
|
||||
@@ -7,12 +7,12 @@ function readStored(): FeedTab {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored && (FEED_TABS as readonly string[]).includes(stored)
|
||||
? stored as FeedTab
|
||||
: "hot";
|
||||
: DEFAULT_FEED_TAB;
|
||||
}
|
||||
|
||||
/**
|
||||
* User preference for which feed tab the index opens on, persisted to
|
||||
* localStorage (defaults to "hot"). Returns the stored tab and a setter that
|
||||
* localStorage (defaults to DEFAULT_FEED_TAB). Returns the stored tab and a setter that
|
||||
* persists the choice. Consumers that only need to honour the preference (e.g.
|
||||
* the index default) can ignore the setter.
|
||||
*/
|
||||
|
||||
@@ -304,7 +304,8 @@ function DumpEditForm({
|
||||
const toggleCategory = (id: string) =>
|
||||
setSelectedCategoryIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
const form = useApiForm<EditValues>({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLocation, useParams } from "react-router";
|
||||
import { AppHeader } from "../components/AppHeader.tsx";
|
||||
import { PresenceRow } from "../components/PresenceRow.tsx";
|
||||
import { FeedTabBar } from "../components/FeedTabBar.tsx";
|
||||
import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
import { DEFAULT_FEED_TAB, FEED_TABS, type FeedTab } from "../config/feedTabs.ts";
|
||||
import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts";
|
||||
|
||||
import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts";
|
||||
@@ -89,7 +89,7 @@ export function Index({ categorySlug }: { categorySlug?: string }) {
|
||||
const { feedTab: rawTab } = useParams();
|
||||
const [preferredTab] = useDefaultFeedTab();
|
||||
const defaultTab: FeedTab = preferredTab === "followed" && !user
|
||||
? "hot"
|
||||
? DEFAULT_FEED_TAB
|
||||
: preferredTab;
|
||||
const tab: FeedTab = (FEED_TABS as readonly string[]).includes(rawTab ?? "")
|
||||
? (rawTab as FeedTab)
|
||||
|
||||
@@ -241,7 +241,7 @@ export function FollowedFeed({
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [token, usersState.status, playlistsState.status]);
|
||||
}, [token, usersState.status, playlistsState.status, catQuery]);
|
||||
|
||||
// Scroll save
|
||||
useScrollSave(
|
||||
@@ -298,7 +298,7 @@ export function FollowedFeed({
|
||||
s.status === "loaded" ? { ...s, loadingMore: false } : s
|
||||
)
|
||||
);
|
||||
}, [usersState, token]);
|
||||
}, [usersState, token, catQuery]);
|
||||
|
||||
const loadMorePlaylists = useCallback(() => {
|
||||
if (
|
||||
@@ -333,7 +333,7 @@ export function FollowedFeed({
|
||||
s.status === "loaded" ? { ...s, loadingMore: false } : s
|
||||
)
|
||||
);
|
||||
}, [playlistsState, token]);
|
||||
}, [playlistsState, token, catQuery]);
|
||||
|
||||
return (
|
||||
<div className="followed-feed">
|
||||
|
||||
Reference in New Issue
Block a user