mirror of
https://github.com/mastodon/mastodon.git
synced 2026-03-14 22:44:08 +00:00
Profile redesign: Account filter fixes (#37811)
This commit is contained in:
parent
66b09318ed
commit
0279a52216
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
64
app/javascript/mastodon/hooks/useStorage.ts
Normal file
64
app/javascript/mastodon/hooks/useStorage.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user