[Glitch] Profile redesign: Account filter fixes
Port 0279a52216b789a759f4f8556827cfb316eaeb6d to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
4fbd1ad7ae
commit
c7b08d8f19
@ -1,28 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useSearchParam } from '@/flavours/glitch/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 '@/flavours/glitch/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 '@/flavours/glitch/hooks/useOverflow';
|
|||||||
import { selectAccountFeaturedTags } from '@/flavours/glitch/selectors/accounts';
|
import { selectAccountFeaturedTags } from '@/flavours/glitch/selectors/accounts';
|
||||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
import { useFilters } from '../hooks/useFilters';
|
import { useAccountContext } from './context';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
|
export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
|
||||||
@ -83,7 +82,7 @@ export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
|
|||||||
function useTagNavigate() {
|
function useTagNavigate() {
|
||||||
// Get current account, tag, and filters.
|
// Get current account, tag, and filters.
|
||||||
const { acct, tagged } = useParams<{ acct: string; tagged?: string }>();
|
const { acct, tagged } = useParams<{ acct: string; tagged?: string }>();
|
||||||
const { boosts, replies } = useFilters();
|
const { boosts, replies } = useAccountContext();
|
||||||
|
|
||||||
const history = useAppHistory();
|
const history = useAppHistory();
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { Icon } from '@/flavours/glitch/components/icon';
|
|||||||
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
||||||
|
|
||||||
import { AccountTabs } from '../components/tabs';
|
import { AccountTabs } from '../components/tabs';
|
||||||
import { useFilters } from '../hooks/useFilters';
|
|
||||||
|
|
||||||
|
import { useAccountContext } from './context';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const AccountFilters: FC = () => {
|
export const AccountFilters: FC = () => {
|
||||||
@ -42,7 +42,7 @@ const FilterDropdown: FC = () => {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { boosts, replies, setBoosts, setReplies } = useFilters();
|
const { boosts, replies, setBoosts, setReplies } = useAccountContext();
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
const { name, checked } = event.target;
|
const { name, checked } = event.target;
|
||||||
@ -101,7 +101,6 @@ const FilterDropdown: FC = () => {
|
|||||||
<Overlay
|
<Overlay
|
||||||
show={open}
|
show={open}
|
||||||
target={buttonRef}
|
target={buttonRef}
|
||||||
flip
|
|
||||||
placement='bottom-start'
|
placement='bottom-start'
|
||||||
rootClose
|
rootClose
|
||||||
onHide={handleHide}
|
onHide={handleHide}
|
||||||
|
|||||||
@ -25,12 +25,11 @@ import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
|||||||
|
|
||||||
import { AccountHeader } from '../components/account_header';
|
import { AccountHeader } from '../components/account_header';
|
||||||
import { LimitedAccountHint } from '../components/limited_account_hint';
|
import { LimitedAccountHint } from '../components/limited_account_hint';
|
||||||
import { useFilters } from '../hooks/useFilters';
|
|
||||||
|
|
||||||
|
import { AccountTimelineProvider, useAccountContext } from './context';
|
||||||
import { FeaturedTags } from './featured_tags';
|
import { FeaturedTags } from './featured_tags';
|
||||||
import { AccountFilters } from './filters';
|
import { AccountFilters } from './filters';
|
||||||
import {
|
import {
|
||||||
PinnedStatusProvider,
|
|
||||||
renderPinnedStatusHeader,
|
renderPinnedStatusHeader,
|
||||||
usePinnedStatusIds,
|
usePinnedStatusIds,
|
||||||
} from './pinned_statuses';
|
} from './pinned_statuses';
|
||||||
@ -56,13 +55,13 @@ const AccountTimelineV2: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
|||||||
|
|
||||||
// Add this key to remount the timeline when accountId changes.
|
// Add this key to remount the timeline when accountId changes.
|
||||||
return (
|
return (
|
||||||
<PinnedStatusProvider>
|
<AccountTimelineProvider accountId={accountId}>
|
||||||
<InnerTimeline
|
<InnerTimeline
|
||||||
accountId={accountId}
|
accountId={accountId}
|
||||||
key={accountId}
|
key={accountId}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
/>
|
/>
|
||||||
</PinnedStatusProvider>
|
</AccountTimelineProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,7 +70,7 @@ const InnerTimeline: FC<{ accountId: string; multiColumn: boolean }> = ({
|
|||||||
multiColumn,
|
multiColumn,
|
||||||
}) => {
|
}) => {
|
||||||
const { tagged } = useParams<{ tagged?: string }>();
|
const { tagged } = useParams<{ tagged?: string }>();
|
||||||
const { boosts, replies } = useFilters();
|
const { boosts, replies } = useAccountContext();
|
||||||
const key = timelineKey({
|
const key = timelineKey({
|
||||||
type: 'account',
|
type: 'account',
|
||||||
userId: accountId,
|
userId: accountId,
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import type { FC, ReactNode } from 'react';
|
import type { FC } from 'react';
|
||||||
import {
|
import { useEffect, useMemo } from 'react';
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@ -28,42 +21,9 @@ import IconPinned from '@/images/icons/icon_pinned.svg?react';
|
|||||||
import { isRedesignEnabled } from '../common';
|
import { isRedesignEnabled } from '../common';
|
||||||
import { PinnedBadge } from '../components/badges';
|
import { PinnedBadge } from '../components/badges';
|
||||||
|
|
||||||
|
import { useAccountContext } from './context';
|
||||||
import classes from './styles.module.scss';
|
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({
|
export function usePinnedStatusIds({
|
||||||
accountId,
|
accountId,
|
||||||
tagged,
|
tagged,
|
||||||
@ -89,7 +49,7 @@ export function usePinnedStatusIds({
|
|||||||
selectTimelineByKey(state, pinnedKey),
|
selectTimelineByKey(state, pinnedKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { showAllPinned } = useContext(PinnedStatusContext);
|
const { showAllPinned } = useAccountContext();
|
||||||
|
|
||||||
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
|
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
|
||||||
const pinnedStatusIds = useMemo(() => {
|
const pinnedStatusIds = useMemo(() => {
|
||||||
@ -125,7 +85,7 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PinnedShowAllButton: FC = () => {
|
export const PinnedShowAllButton: FC = () => {
|
||||||
const { onShowAllPinned } = useContext(PinnedStatusContext);
|
const { onShowAllPinned } = useAccountContext();
|
||||||
|
|
||||||
if (!isRedesignEnabled()) {
|
if (!isRedesignEnabled()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
64
app/javascript/flavours/glitch/hooks/useStorage.ts
Normal file
64
app/javascript/flavours/glitch/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…
x
Reference in New Issue
Block a user