Profile redesign: Account filter fixes (#37811)

This commit is contained in:
Echo 2026-02-10 18:20:02 +01:00 committed by GitHub
parent 66b09318ed
commit 0279a52216
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 174 additions and 84 deletions

View File

@ -1,28 +0,0 @@
import { useCallback } from 'react';
import { useSearchParam } from '@/mastodon/hooks/useSearchParam';
export function useFilters() {
const [boosts, setBoosts] = useSearchParam('boosts');
const [replies, setReplies] = useSearchParam('replies');
const handleSetBoosts = useCallback(
(value: boolean) => {
setBoosts(value ? '1' : null);
},
[setBoosts],
);
const handleSetReplies = useCallback(
(value: boolean) => {
setReplies(value ? '1' : null);
},
[setReplies],
);
return {
boosts: boosts === '1',
replies: replies === '1',
setBoosts: handleSetBoosts,
setReplies: handleSetReplies,
};
}

View File

@ -0,0 +1,97 @@
import type { FC, ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { useStorage } from '@/mastodon/hooks/useStorage';
interface AccountTimelineContextValue {
accountId: string;
boosts: boolean;
replies: boolean;
showAllPinned: boolean;
setBoosts: (value: boolean) => void;
setReplies: (value: boolean) => void;
onShowAllPinned: () => void;
}
const AccountTimelineContext =
createContext<AccountTimelineContextValue | null>(null);
export const AccountTimelineProvider: FC<{
accountId: string;
children: ReactNode;
}> = ({ accountId, children }) => {
const { getItem, setItem } = useStorage({
type: 'session',
prefix: `filters-${accountId}:`,
});
const [boosts, setBoosts] = useState(
() => (getItem('boosts') === '0' ? false : true), // Default to enabled.
);
const [replies, setReplies] = useState(() =>
getItem('replies') === '1' ? true : false,
);
const handleSetBoosts = useCallback(
(value: boolean) => {
setBoosts(value);
setItem('boosts', value ? '1' : '0');
},
[setBoosts, setItem],
);
const handleSetReplies = useCallback(
(value: boolean) => {
setReplies(value);
setItem('replies', value ? '1' : '0');
},
[setReplies, setItem],
);
const [showAllPinned, setShowAllPinned] = useState(false);
const handleShowAllPinned = useCallback(() => {
setShowAllPinned(true);
}, []);
// Memoize the context value to avoid unnecessary re-renders.
const value = useMemo(
() => ({
accountId,
boosts,
replies,
showAllPinned,
setBoosts: handleSetBoosts,
setReplies: handleSetReplies,
onShowAllPinned: handleShowAllPinned,
}),
[
accountId,
boosts,
handleSetBoosts,
handleSetReplies,
handleShowAllPinned,
replies,
showAllPinned,
],
);
return (
<AccountTimelineContext.Provider value={value}>
{children}
</AccountTimelineContext.Provider>
);
};
export function useAccountContext() {
const values = useContext(AccountTimelineContext);
if (!values) {
throw new Error(
'useAccountFilters must be used within an AccountTimelineProvider',
);
}
return values;
}

View File

@ -13,8 +13,7 @@ import { useOverflowButton } from '@/mastodon/hooks/useOverflow';
import { selectAccountFeaturedTags } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { useFilters } from '../hooks/useFilters';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
@ -83,7 +82,7 @@ export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
function useTagNavigate() {
// Get current account, tag, and filters.
const { acct, tagged } = useParams<{ acct: string; tagged?: string }>();
const { boosts, replies } = useFilters();
const { boosts, replies } = useAccountContext();
const history = useAppHistory();

View File

@ -12,8 +12,8 @@ import { Icon } from '@/mastodon/components/icon';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import { AccountTabs } from '../components/tabs';
import { useFilters } from '../hooks/useFilters';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
export const AccountFilters: FC = () => {
@ -42,7 +42,7 @@ const FilterDropdown: FC = () => {
setOpen(false);
}, []);
const { boosts, replies, setBoosts, setReplies } = useFilters();
const { boosts, replies, setBoosts, setReplies } = useAccountContext();
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
const { name, checked } = event.target;
@ -101,7 +101,6 @@ const FilterDropdown: FC = () => {
<Overlay
show={open}
target={buttonRef}
flip
placement='bottom-start'
rootClose
onHide={handleHide}

View File

@ -25,12 +25,11 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountHeader } from '../components/account_header';
import { LimitedAccountHint } from '../components/limited_account_hint';
import { useFilters } from '../hooks/useFilters';
import { AccountTimelineProvider, useAccountContext } from './context';
import { FeaturedTags } from './featured_tags';
import { AccountFilters } from './filters';
import {
PinnedStatusProvider,
renderPinnedStatusHeader,
usePinnedStatusIds,
} from './pinned_statuses';
@ -56,13 +55,13 @@ const AccountTimelineV2: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
// Add this key to remount the timeline when accountId changes.
return (
<PinnedStatusProvider>
<AccountTimelineProvider accountId={accountId}>
<InnerTimeline
accountId={accountId}
key={accountId}
multiColumn={multiColumn}
/>
</PinnedStatusProvider>
</AccountTimelineProvider>
);
};
@ -71,7 +70,7 @@ const InnerTimeline: FC<{ accountId: string; multiColumn: boolean }> = ({
multiColumn,
}) => {
const { tagged } = useParams<{ tagged?: string }>();
const { boosts, replies } = useFilters();
const { boosts, replies } = useAccountContext();
const key = timelineKey({
type: 'account',
userId: accountId,

View File

@ -1,12 +1,5 @@
import type { FC, ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { FC } from 'react';
import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
@ -28,42 +21,9 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
import { PinnedBadge } from '../components/badges';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
const PinnedStatusContext = createContext<{
showAllPinned: boolean;
onShowAllPinned: () => void;
}>({
showAllPinned: false,
onShowAllPinned: () => {
throw new Error('No onShowAllPinned provided');
},
});
export const PinnedStatusProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
const [showAllPinned, setShowAllPinned] = useState(false);
const handleShowAllPinned = useCallback(() => {
setShowAllPinned(true);
}, []);
// Memoize so the context doesn't change every render.
const value = useMemo(
() => ({
showAllPinned,
onShowAllPinned: handleShowAllPinned,
}),
[handleShowAllPinned, showAllPinned],
);
return (
<PinnedStatusContext.Provider value={value}>
{children}
</PinnedStatusContext.Provider>
);
};
export function usePinnedStatusIds({
accountId,
tagged,
@ -89,7 +49,7 @@ export function usePinnedStatusIds({
selectTimelineByKey(state, pinnedKey),
);
const { showAllPinned } = useContext(PinnedStatusContext);
const { showAllPinned } = useAccountContext();
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
const pinnedStatusIds = useMemo(() => {
@ -125,7 +85,7 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
};
export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useContext(PinnedStatusContext);
const { onShowAllPinned } = useAccountContext();
if (!isRedesignEnabled()) {
return null;

View File

@ -0,0 +1,64 @@
import { useCallback, useMemo } from 'react';
export function useStorage({
type = 'local',
prefix = '',
}: { type?: 'local' | 'session'; prefix?: string } = {}) {
const storageType = type === 'local' ? 'localStorage' : 'sessionStorage';
const isAvailable = useMemo(
() => storageAvailable(storageType),
[storageType],
);
const getItem = useCallback(
(key: string) => {
if (!isAvailable) {
return null;
}
try {
return window[storageType].getItem(prefix ? `${prefix};${key}` : key);
} catch {
return null;
}
},
[isAvailable, storageType, prefix],
);
const setItem = useCallback(
(key: string, value: string) => {
if (!isAvailable) {
return;
}
try {
window[storageType].setItem(prefix ? `${prefix};${key}` : key, value);
} catch {}
},
[isAvailable, storageType, prefix],
);
return {
isAvailable,
getItem,
setItem,
};
}
// Tests the storage availability for the given type. Taken from MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
export function storageAvailable(type: 'localStorage' | 'sessionStorage') {
let storage;
try {
storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e instanceof DOMException &&
e.name === 'QuotaExceededError' &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
}